Art of Pr0gr4m

[Linux Kernel 5] Crypto Device Driver 본문

IT/Linux Kernel

[Linux Kernel 5] Crypto Device Driver

pr0gr4m 2020. 4. 30. 19:18

이번 포스트에선 리눅스 커널에서 제공하는 CryptoAPI를 이용한 문자 디바이스 드라이버를 작성한다

 

 

1. CryptoAPI

 

리눅스 커널에서 제공하는 암호화 관련 인터페이스를 CryptoAPI라고 한다

include/crypto 디렉토리 하위의 헤더 파일을 인클루드하여 사용할 수 있다

 

커널 2.6 버전에서는 대부분의 암호화 알고리즘을 비슷한 인터페이스로 이용했는데

현재는 지원 대상도 다양해지고 기능도 많아져서 꽤나 복잡해졌다

 

해당 포스트는 CryptoAPI 자체에 대해 집중하기보단,

CryptoAPI를 사용하는 문자 디바이스 드라이버 작성이 주이기 때문에

자세한 내용은 다음 링크(Crypto API, Crypto Architecture)에 맡기고

사용하기 쉬운 MD5 해쉬 알고리즘으로 예제를 작성한다

 

 

 

2. MD5 API

 

MD5는 해쉬 함수로, CryptoAPI중 Hash API로 제공된다

Hash API의 상세 내용은 다음 링크를 참고한다

이 중 이번 예제에서 사용할 동기 함수의 프로토타입은 다음과 같다

 

/**
 * crypto_alloc_shash() - allocate message digest handle
 * @alg_name: is the cra_name / name or cra_driver_name / driver name of the
 *	      message digest cipher
 * @type: specifies the type of the cipher
 * @mask: specifies the mask for the cipher
 *
 * Allocate a cipher handle for a message digest. The returned &struct
 * crypto_shash is the cipher handle that is required for any subsequent
 * API invocation for that message digest.
 *
 * Return: allocated cipher handle in case of success; IS_ERR() is true in case
 *	   of an error, PTR_ERR() returns the error code.
 */
struct crypto_shash *crypto_alloc_shash(const char *alg_name, u32 type,
					u32 mask);
 
/**
 * crypto_shash_descsize() - obtain the operational state size
 * @tfm: cipher handle
 *
 * The size of the operational state the cipher needs during operation is
 * returned for the hash referenced with the cipher handle. This size is
 * required to calculate the memory requirements to allow the caller allocating
 * sufficient memory for operational state.
 *
 * The operational state is defined with struct shash_desc where the size of
 * that data structure is to be calculated as
 * sizeof(struct shash_desc) + crypto_shash_descsize(alg)
 *
 * Return: size of the operational state
 */
static inline unsigned int crypto_shash_descsize(struct crypto_shash *tfm)
{
	return tfm->descsize;
}
 
/**
 * crypto_shash_init() - (re)initialize message digest
 * @desc: operational state handle that is already filled
 *
 * The call (re-)initializes the message digest referenced by the
 * operational state handle. Any potentially existing state created by
 * previous operations is discarded.
 *
 * Context: Any context.
 * Return: 0 if the message digest initialization was successful; < 0 if an
 *	   error occurred
 */
static inline int crypto_shash_init(struct shash_desc *desc)
{
	struct crypto_shash *tfm = desc->tfm;
 
	if (crypto_shash_get_flags(tfm) & CRYPTO_TFM_NEED_KEY)
		return -ENOKEY;
 
	return crypto_shash_alg(tfm)->init(desc);
}
 
/**
 * crypto_shash_update() - add data to message digest for processing
 * @desc: operational state handle that is already initialized
 * @data: input data to be added to the message digest
 * @len: length of the input data
 *
 * Updates the message digest state of the operational state handle.
 *
 * Context: Any context.
 * Return: 0 if the message digest update was successful; < 0 if an error
 *	   occurred
 */
int crypto_shash_update(struct shash_desc *desc, const u8 *data,
			unsigned int len);
 
/**
 * crypto_shash_final() - calculate message digest
 * @desc: operational state handle that is already filled with data
 * @out: output buffer filled with the message digest
 *
 * Finalize the message digest operation and create the message digest
 * based on all data added to the cipher handle. The message digest is placed
 * into the output buffer. The caller must ensure that the output buffer is
 * large enough by using crypto_shash_digestsize.
 *
 * Context: Any context.
 * Return: 0 if the message digest creation was successful; < 0 if an error
 *	   occurred
 */
int crypto_shash_final(struct shash_desc *desc, u8 *out);
 
/**
 * crypto_shash_digestsize() - obtain message digest size
 * @tfm: cipher handle
 *
 * The size for the message digest created by the message digest cipher
 * referenced with the cipher handle is returned.
 *
 * Return: digest size of cipher
 */
static inline unsigned int crypto_shash_digestsize(struct crypto_shash *tfm)
{
	return crypto_shash_alg(tfm)->digestsize;
}
 
/**
 * crypto_free_shash() - zeroize and free the message digest handle
 * @tfm: cipher handle to be freed
 */
static inline void crypto_free_shash(struct crypto_shash *tfm)
{
	crypto_destroy_tfm(tfm, crypto_shash_tfm(tfm));
}

(참고로 비동기 함수는 shash가 아닌 ahash이다)

 

호출은 대략 crypto_alloc_shash -> kmalloc(shash_desc) -> crypto_shash_init -> crypto_shash_update -> crypto_shash_final -> crypto_free_shash 순서로 진행된다

 

function desc
crypto_alloc_shash 메시지 다이제스트 핸들 할당 (tfm 구조체는 따로 kmalloc으로 할당)
crypto_shash_init 메시지 다이제스트 초기화
crypto_shash_update 메시지 다이제스트에 프로세싱할 데이터 추가
crypto_shash_final 메시지 다이제스트 계산 (실제 해시 계산)
crypto_free_shash 메시지 다이제스트 핸들 반환

 

이 외 알고리즘 등록 등을 위한 crypto_register_shash 등은 위의 소스 코드를 참고한다

 

 

 

3. MD5 문자 디바이스 드라이버 예제 소스

 

#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/mm.h>
#include <linux/uaccess.h>
#include <linux/scatterlist.h>
#include <crypto/hash.h>
 
#define MINOR_BASE			5
#define MINOR_NUM			1
 
#define DEV_NAME			"md5"
#define MAX_LEN				1024
#define MD5_LEN				16
 
static unsigned char plaintext[1024];
 
static bool hash_md5(char *result, unsigned char *data, size_t len);
static void hexdump(unsigned char *buf, unsigned int len);
static char *hexdump_as_str(unsigned char *buf, unsigned int len);
 
static int virtual_device_open(struct inode *, struct file *);
static int virtual_device_release(struct inode *, struct file *);
static ssize_t virtual_device_write(struct file *, const char *, size_t, loff_t *);
static ssize_t virtual_device_read(struct file *, char *, size_t, loff_t *);
 
static struct class *virtual_device_class = NULL;
static struct cdev *virtual_device = NULL;
 
static atomic_t virtual_device_usage = ATOMIC_INIT(1);
static int virtual_device_major = 0;
static char *buffer = NULL;
 
static struct file_operations vd_fops = {
	.read = virtual_device_read,
	.write = virtual_device_write,
	.open = virtual_device_open,
	.release = virtual_device_release
};
 
static int __init md5_init(void)
{
	int ret = 0;
	dev_t dev;
 
	virtual_device = cdev_alloc();
	ret = alloc_chrdev_region(&dev, MINOR_BASE, MINOR_NUM, DEV_NAME);
	if (ret != 0) {
		printk(KERN_ERR "alloc_chrdev_region = %d\n", ret);
		return -1;
	}
 
	virtual_device_major = MAJOR(dev);
	dev = MKDEV(virtual_device_major, MINOR_BASE);
	printk("[Message] major num: %d\n", virtual_device_major);
 
	cdev_init(virtual_device, &vd_fops);
	virtual_device->owner = THIS_MODULE;
 
	ret = cdev_add(virtual_device, dev, MINOR_NUM);
	if (ret != 0) {
		printk(KERN_ERR "cdev_add = %d\n", ret);
		goto OUT2;
	}
 
	virtual_device_class = class_create(THIS_MODULE, "md5_virtual_device");
	if (IS_ERR(virtual_device_class)) {
		printk(KERN_ERR "class_create\n");
		goto OUT;
	}
 
	device_create(virtual_device_class, NULL, MKDEV(virtual_device_major, MINOR_BASE), NULL, DEV_NAME);
 
	buffer = (char *)kzalloc(64, GFP_KERNEL);
	if (buffer == NULL) {
		printk(KERN_ERR "[Message] kzalloc failed\n");
		goto OUT;
	}
 
	return 0;
 
OUT:
	cdev_del(virtual_device);
OUT2:
	unregister_chrdev_region(dev, MINOR_NUM);
	return -1;
}
 
static void __exit md5_exit(void)
{
	dev_t dev = MKDEV(virtual_device_major, MINOR_BASE);
 
	device_destroy(virtual_device_class, dev);
	class_destroy(virtual_device_class);
	cdev_del(virtual_device);
	unregister_chrdev_region(dev, MINOR_NUM);
}
 
 
static int virtual_device_open(struct inode *inode, struct file *filp)
{
	printk("[Message] before open: %d\n", virtual_device_usage.counter);
 
	if (!atomic_dec_and_test(&virtual_device_usage)) {
		// already used..
		atomic_inc(&virtual_device_usage);
		return -EBUSY;
	}
	printk("[Message] after open: %d\n", virtual_device_usage.counter);
	return 0;
}
 
static int virtual_device_release(struct inode *inode, struct file *filp)
{
	printk("[Message] before release: %d\n", virtual_device_usage.counter);
	atomic_inc(&virtual_device_usage);
	printk("[Message] after release: %d\n", virtual_device_usage.counter);
	return 0;
}
 
static ssize_t virtual_device_write(struct file *filp, const char __user *buf, 
		size_t count, loff_t *f_pos)
{
	printk("[Message] write function called\n");
	copy_from_user(plaintext, buf, count);
	return count;
}
 
static ssize_t virtual_device_read(struct file *filp, char __user *buf, 
		size_t count, loff_t *f_pos)
{
	char *p = (char *)kzalloc(MD5_LEN, GFP_KERNEL);
	printk("[Message] read function called\n");
	hash_md5(p, plaintext, strlen(plaintext));
	copy_to_user(buf, p, MD5_LEN);
	kfree(p);
	return count;
}
 
static void hexdump(unsigned char *buf, unsigned int len)
{
	printk("[Message] ");
	while (len--)
		printk("%02x", *buf++);
	printk("\n");
}
 
static char *hexdump_as_str(unsigned char *buf, unsigned int len)
{
	char *str = (char *)kmalloc(len, GFP_KERNEL);
	while (len--)
		strcat(str++, buf++);
	return str;
}
 
static bool hash_md5(char *result, unsigned char *data, size_t len)
{
	int i, success = 0;
	struct crypto_shash *shash = NULL;
	struct shash_desc *sdesc = NULL;
	bool ret = true;
 
	printk("hash_md5 function called\n");
 
	shash = crypto_alloc_shash("md5", 0, 0);
	if (IS_ERR(shash))
		goto EXIT_ERROR;
	sdesc = kmalloc(sizeof(struct shash_desc) + crypto_shash_descsize(shash),
				GFP_KERNEL);
	if (sdesc == NULL)
		goto EXIT_ERROR;
 
	sdesc->tfm = shash;
 
	success = crypto_shash_init(sdesc);
	if (success < 0)
		goto EXIT_ERROR;
 
	success = crypto_shash_update(sdesc, data, len);
	if (success < 0)
		goto EXIT_ERROR;
 
	success = crypto_shash_final(sdesc, result);
	if (success < 0)
		goto EXIT_ERROR;
 
	printk("[Message] ---------- test md5 ----------\n");
	i = crypto_shash_digestsize(sdesc->tfm);
	hexdump(result, i);
	printk("[Message] Length: %d\n", i);
	goto EXIT;
 
EXIT_ERROR:
	ret = false;
EXIT:
	if (sdesc)
		kfree(sdesc);
	if (shash)
		crypto_free_shash(shash);
 
	return ret;
}
 
module_init(md5_init);
module_exit(md5_exit);
MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("pr0gr4m");
MODULE_DESCRIPTION("MD5 hash device");

 

디바이스 드라이버와 오퍼레이션 등록/해제 과정은 이 전 포스트들을 참고한다

 

한번에 하나의 프로세스에서만 해당 디바이스 드라이버를 사용할 수 있도록

open/release 함수에서는 atomic 변수를 dec&test / inc 한다

write 함수에서는 유저 프로세스에서 전달한 데이터 (plaintext)를 커널에 저장한다

read 함수에서는 md5 해시값을 계산하여 유저 프로세스에게 넘겨준다

 

hexdump 함수는 단순히 헥스값을 출력한다

실제 md5 해시값을 계산하는 함수는 hash_md5이다

함수의 설명과 사용 방법은 예제 소스와 2번 표를 참고한다

 

 

 

4. 테스트 코드

 

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
 
#define DEV_NAME	"/dev/md5"
#define MAX_LEN		16
 
static void hexdump(unsigned char *buf, unsigned int len)
{
	while (len--)
		printf("%02x", *buf++);
	printf("\n");
}
 
int main(int argc, char *argv[])
{
	int dev;
	char buf[MAX_LEN];
 
	if (argc < 2) {
		printf("Usage: %s data\n", argv[0]);
		exit(EXIT_FAILURE);
	}
 
	memset(buf, 0, MAX_LEN);
	puts("Device driver test");
 
	if ((dev = open(DEV_NAME, O_RDWR)) < 0) {
		perror("open error");
		exit(EXIT_FAILURE);
	}
 
	write(dev, argv[1], strlen(argv[1]));
	read(dev, buf, MAX_LEN);
 
	hexdump(buf, 16);
 
	close(dev);
	return 0;
}

 

프로그램의 인자로 전달한 문자열을 md5 디바이스 드라이버에 write한 후 read 하여 hexdump로 출력한다

빌드를 위한 Makefile은 이 전 포스트를 참고한다

 

 

 

5. 실행 결과

 

md5_dev 테스트 결과
dmesg 결과

test_string 문자열을 128비트(16바이트)의 MD5 해시값으로 변환하여 출력한 것을 볼 수 있다

또한 dmesg 결과를 보면 open/release 시 usage 값을 정상적으로 업데이트 한 걸 볼 수 있다

 

 

 

실제로 해시 함수를 사용할 경우 MD5보다 안전한 SHA 알고리즘을 사용하는 것을 추천한다

또한, 암호화 파일 시스템 작성 등을 위해 블록 암호화 알고리즘이 필요하다면 AES와 같은 알고리즘을 사용하면 된다