암호화 드라이버 구현 가이드 (Crypto Driver Implementation)
리눅스 커널 Crypto API에서 새로운 암호화 알고리즘을 드라이버로 구현하는 방법을 심층 분석합니다. crypto_alg 구조체와 드라이버 인터페이스(shash, skcipher, AEAD, akcipher, kpp), 알고리즘 등록 흐름과 우선순위 선택, 템플릿 인스턴스 생성, AEAD 상태 머신, FIPS 모드 고려사항, 테스트 프레임워크(tcrypt, testmgr), 성능 추적(ftrace, bpftrace, perf), 실전 구현 패턴까지 포괄합니다.
핵심 요약
- 인터페이스 선택 — 알고리즘 유형에 맞는 드라이버 인터페이스(shash, skcipher, aead 등)를 선택합니다.
- crypto_alg 등록 — 알고리즘 이름, 우선순위, 블록 크기 등 메타데이터를 올바르게 설정합니다.
- 컨텍스트 생명주기 — tfm 컨텍스트와 request 컨텍스트의 할당·해제 시점을 정확히 관리합니다.
- 비동기 반환 계약 — 동기 완료 시 0, 비동기 진행 시 -EINPROGRESS, 큐잉 시 -EBUSY를 반환합니다.
- 테스트 필수 — testmgr 테스트 벡터와 tcrypt 벤치마크로 정확성과 성능을 검증합니다.
단계별 이해
- 인터페이스 결정
구현할 알고리즘 유형에 맞는 드라이버 API를 선택합니다. - 구조체 정의
crypto_alg과 드라이버 콜백 함수를 구현합니다. - 등록과 테스트
알고리즘을 등록하고 testmgr로 정확성을 검증합니다. - 성능 계측
tcrypt, ftrace, perf로 성능을 측정하고 최적화합니다.
알고리즘 구현 가이드
커널 Crypto Framework (Crypto API)에 새로운 알고리즘을 추가하려면 알고리즘 구조체(Struct) 정의 → 콜백 구현 → 등록 → 테스트 벡터 추가의 과정을 거칩니다. 이 섹션에서는 각 단계를 상세히 다룹니다.
드라이버 작성자가 먼저 결정할 것: 어떤 인터페이스를 구현할 것인가
Crypto driver 작성에서 가장 먼저 해야 할 일은 "이 하드웨어가 AES를 지원합니다"가 아니라 어떤 추상 인터페이스를 커널에 노출할지를 정하는 것입니다. 같은 AES 엔진이라도 단일 블록만 처리하는지, 모드(CBC/CTR/XTS/GCM)까지 자체 처리하는지, AAD와 tag를 함께 계산하는지, DMA로 긴 SG를 직접 읽는지에 따라 등록해야 하는 타입이 달라집니다.
| 인터페이스 | 언제 선택하나 | 장점 | 주의점 |
|---|---|---|---|
struct crypto_alg + cipher_alg | 하드웨어가 "단일 블록 암복호화" primitive만 제공할 때 | 가장 낮은 레벨이라 조합성이 높음 | 대부분의 상위 사용자는 직접 raw block cipher를 안 쓰므로, 보통 skcipher/aead 래퍼를 추가로 제공해야 합니다. |
struct skcipher_alg | CBC/CTR/XTS 같은 대칭 암호 모드를 직접 제공할 때 | dm-crypt, fscrypt, 네트워크 경로가 바로 사용 가능 | IV, alignmask, walksize, fallback 설계를 함께 해야 합니다. |
struct aead_alg | GCM/CCM/ChaCha20-Poly1305처럼 인증 태그까지 한 번에 처리할 때 | IPsec, kTLS, 보안 저장 포맷과 직접 맞닿음 | AAD 길이와 tag 길이 규약을 틀리면 전체가 깨집니다. |
struct shash_alg | 순수 소프트웨어 해시 또는 즉시 계산 가능한 작은 엔진 | 구현이 가장 단순 | SG/DMA 친화성은 약합니다. |
struct ahash_alg | DMA 기반 해시 엔진, 긴 SG 스트림, 비동기 완료가 필요할 때 | 네트워크/파일/펌웨어처럼 조각 난 입력에 강함 | reqsize, 상태 export/import, one-shot 제약을 명확히 해야 합니다. |
struct akcipher_alg | RSA 같은 공개키 암/복호화 또는 서명 primitive를 제공할 때 | 키 설정, max size, 비동기 요청을 통합 제공 | req->dst_len 갱신 계약을 지켜야 합니다. |
struct kpp_alg | DH/ECDH처럼 공개키 생성과 공유 비밀 계산을 제공할 때 | 핸드셰이크 계층과 직접 연결됨 | packed secret 형식과 공개키 출력 길이 계산이 중요합니다. |
struct rng_alg | TRNG/DRBG 엔진을 제공할 때 | FIPS 경계, 정책형 RNG 선택에 유리 | seedsize와 재시드 의미를 명확히 해야 합니다. |
struct acomp_alg | 압축 가속기를 비동기로 제공할 때 | 압축 엔진도 Crypto queue/model을 재사용 가능 | dst == NULL일 때 출력 버퍼 할당/해제를 정확히 구현해야 합니다. |
struct crypto_template | 기존 알고리즘을 감싸 새 이름을 동적으로 만들 때 | 한 구현으로 다수의 조합 인스턴스를 만들 수 있음 | spawn, 이름 조합, instance free 경로를 잘못 짜면 해제 경합(Contention)이 생깁니다. |
skcipher를 구현할 필요는 없습니다.
어떤 엔진은 먼저 raw block cipher를 안정적으로 올리고, 그 위에 CBC/XTS/GCM glue를 얹는 편이 유지보수에 더 좋습니다.
반대로 하드웨어가 GCM tag까지 완전히 계산한다면 raw block layer를 굳이 외부에 노출하지 않고 aead만 등록하는 편이 더 자연스럽습니다.
struct crypto_alg — 알고리즘 등록의 핵심
모든 암호화 알고리즘은 struct crypto_alg을 기반으로 등록됩니다. 이 구조체는 알고리즘의 메타데이터와 구현 콜백을 담는 컨테이너(Container)입니다:
/* include/linux/crypto.h */
struct crypto_alg {
struct list_head cra_list; /* 내부 연결 리스트 */
struct list_head cra_users; /* 이 알고리즘을 사용하는 tfm 목록 */
u32 cra_flags; /* 알고리즘 속성 플래그 */
unsigned int cra_blocksize; /* 블록 크기 (해시: 입력 블록, 암호: 블록 크기) */
unsigned int cra_ctxsize; /* tfm 컨텍스트 크기 (키 상태 등) */
unsigned int cra_alignmask; /* 데이터 정렬 요구사항 (0 = 1바이트 정렬) */
int cra_priority; /* 우선순위: 높을수록 먼저 선택 */
unsigned int cra_refcnt; /* 참조 카운터 */
char cra_name[128]; /* 알고리즘 정규 이름 ("sha256") */
char cra_driver_name[128]; /* 드라이버 고유 이름 ("sha256-avx2") */
const struct crypto_type *cra_type; /* 알고리즘 유형 (skcipher, shash 등) */
union {
struct cipher_alg cipher; /* 단일 블록 암호 (raw block cipher) */
} cra_u;
int (*cra_init)(struct crypto_tfm *tfm); /* tfm 생성 시 초기화 */
void (*cra_exit)(struct crypto_tfm *tfm); /* tfm 해제 시 정리 */
void (*cra_destroy)(struct crypto_alg *alg); /* 알고리즘 등록 해제 시 */
struct module *cra_module; /* 소속 커널 모듈 */
};
코드 설명
include/linux/crypto.h에 정의된 struct crypto_alg는 커널 Crypto API에 등록되는 모든 암호 알고리즘의 기본 메타데이터 컨테이너입니다.
- cra_list / cra_users전역 알고리즘 리스트(
crypto_alg_list)와 이 알고리즘으로 생성된 tfm 인스턴스 목록을 연결하는 내부 리스트 헤드입니다. - cra_flags
CRYPTO_ALG_ASYNC(비동기),CRYPTO_ALG_NEED_FALLBACK(S/W 대체 필요),CRYPTO_ALG_INTERNAL(내부 전용) 등 알고리즘 속성을 비트마스크로 표현합니다. - cra_ctxsize
crypto_alloc_*()호출 시 tfm에 함께 할당될 드라이버별 컨텍스트 크기입니다. 키 스케줄, H/W 핸들 등을 저장하는 공간으로 사용됩니다. - cra_priority같은
cra_name을 가진 여러 구현 중 이 값이 가장 높은 것이 자동 선택됩니다. generic(100) < SIMD(300) < H/W(4001) 순이 일반적입니다. - cra_name / cra_driver_name
cra_name은 소비자가 요청하는 표준 이름("sha256"),cra_driver_name은 구현체 고유 식별자("sha256-avx2")입니다. - cra_init / cra_exittfm 인스턴스 생성·해제 시 호출되는 콜백으로, fallback tfm 할당이나 H/W 리소스 초기화·정리에 사용됩니다.
- cra_module이 알고리즘이 속한 커널 모듈 포인터로, tfm이 존재하는 동안 모듈 언로드를 방지하는 참조 카운팅에 활용됩니다.
주요 필드 상세
| 필드 | 설명 | 예시 값 |
|---|---|---|
cra_name | 사용자가 요청하는 정규 이름. 같은 이름의 여러 구현이 공존 가능 | "sha256", "aes" |
cra_driver_name | 구현체를 고유하게 식별하는 이름 | "sha256-avx2", "aes-aesni" |
cra_priority | 같은 cra_name 중 가장 높은 priority가 선택됨 | generic: 100, AES-NI: 400, QAT: 4001 |
cra_flags | 알고리즘 속성 비트마스크 | CRYPTO_ALG_ASYNC, CRYPTO_ALG_NEED_FALLBACK |
cra_blocksize | 처리 단위 크기 (바이트) | AES: 16, SHA-256: 64, 스트림 암호: 1 |
cra_ctxsize | tfm당 할당할 private 컨텍스트 크기 | sizeof(struct my_alg_ctx) |
cra_alignmask | 데이터 정렬 마스크. 예: 0x3이면 4바이트 정렬 필요 | 보통 0 (S/W) 또는 0xf (H/W) |
cra_flags 주요 플래그
| 플래그 | 설명 |
|---|---|
CRYPTO_ALG_ASYNC | 비동기 알고리즘. 완료 콜백으로 결과를 통보. H/W 가속기에서 주로 사용 |
CRYPTO_ALG_NEED_FALLBACK | 일부 입력을 처리하지 못할 때 S/W fallback이 필요함을 표시 |
CRYPTO_ALG_KERN_DRIVER_ONLY | 커널 내부 드라이버 전용 구현. ISA나 사용자 공간 인터페이스로 직접 노출하는 것을 막고 싶을 때 사용 |
CRYPTO_ALG_INTERNAL | 내부 전용 알고리즘. AF_ALG(사용자 공간)에 노출되지 않음 |
CRYPTO_ALG_ALLOCATES_MEMORY | 요청 처리 중 메모리 할당이 일어날 수 있음을 의미. alignmask/page 경계 제약을 만족하지 못하면 re-align/임시 버퍼가 생길 수 있음 |
CRYPTO_ALG_FIPS_INTERNAL | 자체로는 승인 알고리즘이 아니지만 FIPS 승인 상위 알고리즘의 내부 부품으로만 쓰이게 할 때 사용 |
CRYPTO_ALG_TESTED | testmgr 자가 테스트를 통과한 뒤 커널이 세우는 내부 상태 비트. 드라이버가 직접 넣는 플래그가 아닙니다. |
CRYPTO_ALG_OPTIONAL_KEY | 키 설정이 선택사항 (예: 키 없는 해시) |
CRYPTO_ALG_DEAD | 해제 진행 중인 알고리즘 (내부 사용) |
이름, 우선순위, 모듈 alias를 어떻게 설계할 것인가
실제 driver author 입장에서 가장 먼저 엉키는 지점은 연산 코드가 아니라 이름 체계와 노출 정책입니다. cra_name은 커널 전체가 공유하는 공개 계약이고, cra_driver_name은 특정 구현체를 식별하는 내부 이름입니다. 이 둘을 섞어 쓰면 autoload, fallback, testmgr, 우선순위 선택이 전부 헷갈립니다.
| 항목 | 권장 기준 | 잘못 설계했을 때의 문제 |
|---|---|---|
cra_name | 소비자가 요청할 표준 이름을 그대로 사용합니다. 예: "ctr(aes)", "sha256", "gcm(aes)" | 벤더명을 섞어 "mychip-gcm"처럼 만들면 상위 subsystem이 해당 구현을 자동 선택하지 못합니다. |
cra_driver_name | 벤더/ISA/세부 구현을 식별하도록 구체적으로 씁니다. 예: "gcm-aes-myhw", "sha256-avx2" | driver name이 모호하면 /proc/crypto, 로그, 자가 테스트 실패 메시지에서 원인 추적이 어려워집니다. |
| 내부 helper 이름 | 직접 소비시키고 싶지 않은 primitive는 "__ctr(aes)"처럼 내부 이름을 따로 두고 CRYPTO_ALG_INTERNAL 또는 CRYPTO_ALG_KERN_DRIVER_ONLY로 숨깁니다. | 외부에 노출되면 AF_ALG나 다른 subsystem이 아직 완성되지 않은 내부 primitive를 직접 잡아 버릴 수 있습니다. |
cra_priority | 같은 cra_name을 가진 다른 구현과의 상대 순서만 의미 있습니다. generic보다 조금 높게, SIMD/HW보다 낮거나 높게 일관되게 정합니다. | 과도하게 높게 잡으면 fallback보다 미완성 H/W 경로가 먼저 선택되고, 너무 낮으면 등록은 됐는데 영원히 선택되지 않습니다. |
MODULE_ALIAS_CRYPTO | 최소한 공개 이름과 driver name 둘 다 alias를 추가합니다. | 모듈 자동 로드가 안 되면 crypto_alloc_*()가 "알고리즘 없음"으로 끝나고 사용자는 원인을 못 찾습니다. |
#include <crypto/internal/skcipher.h>
static struct skcipher_alg my_ctr_algs[] = {
{
.setkey = my_ctr_setkey,
.encrypt = my_ctr_encrypt_internal,
.decrypt = my_ctr_decrypt_internal,
.init = my_ctr_init_tfm,
.exit = my_ctr_exit_tfm,
.min_keysize = 16,
.max_keysize = 32,
.ivsize = 16,
.chunksize = 16,
.walksize = 16,
.base = {
.cra_name = "__ctr(aes)",
.cra_driver_name = "__ctr-aes-myhw",
.cra_priority = 400,
.cra_flags = CRYPTO_ALG_ASYNC |
CRYPTO_ALG_INTERNAL |
CRYPTO_ALG_KERN_DRIVER_ONLY,
.cra_blocksize = 1,
.cra_ctxsize = sizeof(struct my_ctr_ctx),
.cra_module = THIS_MODULE,
},
}, {
.setkey = my_ctr_setkey,
.encrypt = my_ctr_encrypt,
.decrypt = my_ctr_decrypt,
.init = my_ctr_init_tfm,
.exit = my_ctr_exit_tfm,
.min_keysize = 16,
.max_keysize = 32,
.ivsize = 16,
.chunksize = 16,
.walksize = 16,
.base = {
.cra_name = "ctr(aes)",
.cra_driver_name = "ctr-aes-myhw",
.cra_priority = 401,
.cra_flags = CRYPTO_ALG_ASYNC | CRYPTO_ALG_NEED_FALLBACK,
.cra_blocksize = 1,
.cra_ctxsize = sizeof(struct my_ctr_ctx),
.cra_module = THIS_MODULE,
},
},
};
MODULE_ALIAS_CRYPTO("ctr(aes)");
MODULE_ALIAS_CRYPTO("ctr-aes-myhw");
코드 설명
crypto/internal/skcipher.h 기반의 struct skcipher_alg 배열로, 하나의 H/W 드라이버가 내부용과 외부용 두 가지 알고리즘을 동시에 등록하는 패턴입니다.
- 첫 번째 항목 (__ctr(aes))
cra_name이"__ctr(aes)"로 시작하는 이중 밑줄 접두사는 내부 전용 primitive를 의미합니다.CRYPTO_ALG_INTERNAL | CRYPTO_ALG_KERN_DRIVER_ONLY플래그로 AF_ALG 등 외부 접근을 차단합니다. - 두 번째 항목 (ctr(aes))외부에 노출되는 공개 알고리즘으로,
CRYPTO_ALG_NEED_FALLBACK플래그를 설정하여 SIMD 불가 상황에서 S/W 대체를 허용합니다. priority 401은 내부 알고리즘(400)보다 1 높게 설정됩니다. - setkey / encrypt / decrypt
skcipher_alg의 핵심 콜백들로, 키 설정과 암·복호화 연산을 구현합니다. 내부/외부 알고리즘이 서로 다른 함수(my_ctr_encrypt_internalvsmy_ctr_encrypt)를 지정할 수 있습니다. - min_keysize / max_keysize / ivsizeAES-128/192/256을 지원하기 위해 키 크기 범위(16~32바이트)와 CTR 모드의 IV 크기(16바이트)를 지정합니다.
- MODULE_ALIAS_CRYPTO공개 이름과 드라이버 이름 모두에 대해 모듈 alias를 등록하여,
crypto_alloc_skcipher("ctr(aes)")호출 시 커널이 자동으로 이 모듈을modprobe할 수 있게 합니다.
include/crypto/algapi.h는
CRYPTO_ALG_ASYNC, CRYPTO_ALG_NEED_FALLBACK, CRYPTO_ALG_ALLOCATES_MEMORY를
"상속돼야 하는 플래그"로 다룹니다. 내부 알고리즘을 감싸는 템플릿이나 래퍼를 만들 때 이 플래그를 지워 버리면 상위 계층이 잘못된 실행 문맥을 가정하게 됩니다.
shash 알고리즘 구현 (동기 해시)
가장 단순한 구현 유형인 shash (synchronous hash)로 시작합니다. 커널 소스의 crypto/sha256_generic.c를 참고한 전체 구현 예제입니다:
#include <crypto/internal/hash.h>
#include <linux/module.h>
#define MY_HASH_DIGEST_SIZE 32 /* 출력 해시 크기 (바이트) */
#define MY_HASH_BLOCK_SIZE 64 /* 내부 처리 블록 크기 */
/* tfm당 할당되는 컨텍스트 (키, 상태 등) */
struct my_hash_tfm_ctx {
u8 key[32];
bool has_key;
};
/* 요청(desc)당 할당되는 상태 (중간 해시 상태 등) */
struct my_hash_desc_ctx {
u64 state[4];
u8 buf[MY_HASH_BLOCK_SIZE];
unsigned int buflen;
u64 count; /* 처리한 총 바이트 수 */
};
/* ① 해시 초기화: 내부 상태를 초기값으로 설정 */
static int my_hash_init(struct shash_desc *desc)
{
struct my_hash_desc_ctx *dctx = shash_desc_ctx(desc);
dctx->state[0] = 0x6a09e667f3bcc908ULL; /* 초기 해시 값 */
dctx->state[1] = 0xbb67ae8584caa73bULL;
dctx->state[2] = 0x3c6ef372fe94f82bULL;
dctx->state[3] = 0xa54ff53a5f1d36f1ULL;
dctx->buflen = 0;
dctx->count = 0;
return 0;
}
/* ② 데이터 입력: 여러 번 호출될 수 있음 (스트리밍 해시) */
static int my_hash_update(struct shash_desc *desc,
const u8 *data, unsigned int len)
{
struct my_hash_desc_ctx *dctx = shash_desc_ctx(desc);
dctx->count += len;
/* 버퍼에 남은 데이터와 새 데이터를 합쳐 블록 단위로 처리 */
if (dctx->buflen + len < MY_HASH_BLOCK_SIZE) {
memcpy(dctx->buf + dctx->buflen, data, len);
dctx->buflen += len;
return 0;
}
/* 블록 단위 처리 (실제 해시 압축 함수 호출) */
/* my_hash_compress(dctx->state, data_blocks, nblocks); */
return 0;
}
/* ③ 최종 해시 값 출력 */
static int my_hash_final(struct shash_desc *desc, u8 *out)
{
struct my_hash_desc_ctx *dctx = shash_desc_ctx(desc);
/* 패딩 적용 + 마지막 블록 처리 */
/* my_hash_final_block(dctx); */
/* 내부 상태를 출력 형식으로 변환 */
memcpy(out, dctx->state, MY_HASH_DIGEST_SIZE);
return 0;
}
/* ④ 선택사항: 한 번에 처리 (init+update+final 최적화) */
static int my_hash_digest(struct shash_desc *desc,
const u8 *data, unsigned int len,
u8 *out)
{
my_hash_init(desc);
my_hash_update(desc, data, len);
return my_hash_final(desc, out);
}
/* ⑤ 선택사항: 키 설정 (HMAC, keyed hash용) */
static int my_hash_setkey(struct crypto_shash *tfm,
const u8 *key, unsigned int keylen)
{
struct my_hash_tfm_ctx *ctx = crypto_shash_ctx(tfm);
if (keylen > 32)
return -EINVAL;
memcpy(ctx->key, key, keylen);
ctx->has_key = true;
return 0;
}
/* ⑥ 상태 import/export (중간 상태 저장/복원, 선택사항) */
static int my_hash_export(struct shash_desc *desc, void *out)
{
struct my_hash_desc_ctx *dctx = shash_desc_ctx(desc);
memcpy(out, dctx, sizeof(*dctx));
return 0;
}
static int my_hash_import(struct shash_desc *desc, const void *in)
{
struct my_hash_desc_ctx *dctx = shash_desc_ctx(desc);
memcpy(dctx, in, sizeof(*dctx));
return 0;
}
/* ⑦ shash_alg 구조체 정의 */
static struct shash_alg my_hash_alg = {
.init = my_hash_init,
.update = my_hash_update,
.final = my_hash_final,
.digest = my_hash_digest, /* 선택: 한 번에 처리 */
.setkey = my_hash_setkey, /* 선택: keyed hash */
.export = my_hash_export, /* 선택: 상태 저장 */
.import = my_hash_import, /* 선택: 상태 복원 */
.descsize = sizeof(struct my_hash_desc_ctx),
.statesize = sizeof(struct my_hash_desc_ctx),
.digestsize = MY_HASH_DIGEST_SIZE,
.base = {
.cra_name = "myhash256",
.cra_driver_name = "myhash256-generic",
.cra_priority = 100,
.cra_blocksize = MY_HASH_BLOCK_SIZE,
.cra_ctxsize = sizeof(struct my_hash_tfm_ctx),
.cra_module = THIS_MODULE,
}
};
/* ⑧ 모듈 초기화/해제 */
static int __init my_hash_mod_init(void)
{
return crypto_register_shash(&my_hash_alg);
}
static void __exit my_hash_mod_exit(void)
{
crypto_unregister_shash(&my_hash_alg);
}
module_init(my_hash_mod_init);
module_exit(my_hash_mod_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Example hash algorithm");
MODULE_ALIAS_CRYPTO("myhash256");
MODULE_ALIAS_CRYPTO("myhash256-generic");
shash vs ahash: shash는 동기(synchronous) 해시로 구현이 단순합니다. S/W 구현은 shash를 사용하세요. ahash는 비동기(async) 해시로, H/W 가속기처럼 처리 완료까지 대기가 필요한 경우 사용합니다. 커널은 내부적으로 shash를 ahash로 자동 래핑하여 비동기 인터페이스에서도 사용할 수 있게 합니다.
shash 콜백 호출 흐름
skcipher 알고리즘 구현 (대칭 암호)
대칭 블록/스트림 암호의 구현입니다. struct skcipher_alg을 사용하여 등록합니다:
#include <crypto/internal/skcipher.h>
#include <linux/module.h>
#define MY_CIPHER_BLOCK_SIZE 16
#define MY_CIPHER_KEY_SIZE 32
#define MY_CIPHER_IV_SIZE 16
/* tfm 컨텍스트: 확장된 키 스케줄 등 */
struct my_cipher_ctx {
u32 enc_key_sched[60]; /* 암호화 키 스케줄 */
u32 dec_key_sched[60]; /* 복호화 키 스케줄 */
unsigned int key_length;
};
/* 키 설정: 키 스케줄(라운드 키) 생성 */
static int my_cipher_setkey(struct crypto_skcipher *tfm,
const u8 *key, unsigned int keylen)
{
struct my_cipher_ctx *ctx = crypto_skcipher_ctx(tfm);
if (keylen != 16 && keylen != 24 && keylen != 32)
return -EINVAL;
ctx->key_length = keylen;
/* 키 스케줄 확장 수행 */
/* my_expand_key(ctx->enc_key_sched, key, keylen); */
/* my_expand_key_dec(ctx->dec_key_sched, key, keylen); */
return 0;
}
/* 암호화: scatterlist 기반 데이터 처리 */
static int my_cipher_encrypt(struct skcipher_request *req)
{
struct crypto_skcipher *tfm = crypto_skcipher_reqtfm(req);
struct my_cipher_ctx *ctx = crypto_skcipher_ctx(tfm);
struct skcipher_walk walk;
int err;
/* skcipher_walk: scatterlist를 선형 버퍼로 순회하는 헬퍼 */
err = skcipher_walk_virt(&walk, req, false);
while (walk.nbytes) {
unsigned int nbytes = walk.nbytes;
const u8 *src = walk.src.virt.addr;
u8 *dst = walk.dst.virt.addr;
u8 *iv = walk.iv;
/* 블록 단위로 CBC 암호화 수행 */
while (nbytes >= MY_CIPHER_BLOCK_SIZE) {
/* XOR plaintext with IV/previous ciphertext (CBC 모드) */
crypto_xor_cpy(dst, src, iv, MY_CIPHER_BLOCK_SIZE);
/* 블록 암호화 */
/* my_encrypt_block(ctx->enc_key_sched, dst, dst); */
iv = dst;
src += MY_CIPHER_BLOCK_SIZE;
dst += MY_CIPHER_BLOCK_SIZE;
nbytes -= MY_CIPHER_BLOCK_SIZE;
}
/* IV 업데이트 (다음 walk 반복을 위해) */
memcpy(walk.iv, iv, MY_CIPHER_IV_SIZE);
err = skcipher_walk_done(&walk, nbytes);
}
return err;
}
/* 복호화 */
static int my_cipher_decrypt(struct skcipher_request *req)
{
/* 암호화와 유사하나 역방향 처리 + 복호화 키 스케줄 사용 */
/* ... */
return 0;
}
static struct skcipher_alg my_cipher_alg = {
.setkey = my_cipher_setkey,
.encrypt = my_cipher_encrypt,
.decrypt = my_cipher_decrypt,
.min_keysize = 16,
.max_keysize = 32,
.ivsize = MY_CIPHER_IV_SIZE,
.chunksize = MY_CIPHER_BLOCK_SIZE, /* 최소 처리 단위 */
.walksize = MY_CIPHER_BLOCK_SIZE, /* walk 반복당 최소 크기 */
.base = {
.cra_name = "cbc(mycipher)",
.cra_driver_name = "cbc-mycipher-generic",
.cra_priority = 100,
.cra_flags = CRYPTO_ALG_ASYNC, /* H/W인 경우 */
.cra_blocksize = MY_CIPHER_BLOCK_SIZE,
.cra_ctxsize = sizeof(struct my_cipher_ctx),
.cra_module = THIS_MODULE,
}
};
static int __init my_cipher_mod_init(void)
{
return crypto_register_skcipher(&my_cipher_alg);
}
static void __exit my_cipher_mod_exit(void)
{
crypto_unregister_skcipher(&my_cipher_alg);
}
module_init(my_cipher_mod_init);
module_exit(my_cipher_mod_exit);
MODULE_LICENSE("GPL");
MODULE_ALIAS_CRYPTO("cbc(mycipher)");
skcipher_walk 상세
skcipher_walk은 scatterlist 기반의 분산된 데이터를 선형 버퍼처럼 순회하는 핵심 헬퍼입니다:
| 함수 | 설명 | 사용 맥락 |
|---|---|---|
skcipher_walk_virt() | 가상 주소(Virtual Address) 기반 walk 시작 | 일반적인 S/W 구현 |
skcipher_walk_async() | 비동기 요청 컨텍스트에서 walk 시작 | DMA/오프로드 구현 |
skcipher_walk_done() | 현재 chunk 완료, 다음으로 이동 | 반복 루프에서 호출 |
walk.nbytes | 현재 chunk에서 처리 가능한 바이트 수 | 반복 조건 |
walk.src.virt.addr | 입력 데이터 포인터 | 소스 읽기 |
walk.dst.virt.addr | 출력 데이터 포인터 | 결과 쓰기 |
walk.iv | 현재 IV 포인터 | IV 업데이트 |
scatterlist와 zero-copy: 커널의 암호 데이터는 물리적으로 연속되지 않은 페이지에 분산되어 있을 수 있습니다. skcipher_walk은 이런 scatterlist를 임시 선형 버퍼로 매핑(Mapping)하거나, 이미 연속된 경우 직접 포인터를 제공하여 불필요한 복사를 방지합니다.
AEAD 알고리즘 구현
AEAD(Authenticated Encryption with Associated Data)는 암호화와 무결성 검증을 동시에 수행합니다. 구현은 struct aead_alg을 사용합니다:
#include <crypto/internal/aead.h>
#include <crypto/scatterwalk.h>
struct my_aead_ctx {
struct crypto_skcipher *enc_tfm; /* 내부 암호화 tfm */
struct crypto_shash *mac_tfm; /* 내부 MAC tfm */
unsigned int authsize;
};
static int my_aead_setkey(struct crypto_aead *tfm,
const u8 *key, unsigned int keylen)
{
struct my_aead_ctx *ctx = crypto_aead_ctx(tfm);
/* 키를 암호화용과 MAC용으로 분리 */
struct crypto_authenc_keys keys;
if (crypto_authenc_extractkeys(&keys, key, keylen))
return -EINVAL;
/* 각 내부 알고리즘에 키 설정 */
crypto_skcipher_setkey(ctx->enc_tfm, keys.enckey, keys.enckeylen);
crypto_shash_setkey(ctx->mac_tfm, keys.authkey, keys.authkeylen);
return 0;
}
static int my_aead_setauthsize(struct crypto_aead *tfm,
unsigned int authsize)
{
struct my_aead_ctx *ctx = crypto_aead_ctx(tfm);
if (authsize > 16)
return -EINVAL;
ctx->authsize = authsize;
return 0;
}
static int my_aead_encrypt(struct aead_request *req)
{
struct crypto_aead *tfm = crypto_aead_reqtfm(req);
struct my_aead_ctx *ctx = crypto_aead_ctx(tfm);
/* 1단계: 평문을 암호화 (cbc, ctr 등) */
/* 2단계: AAD + 암호문에 대해 MAC 계산 */
/* 3단계: 인증 태그를 출력 끝에 추가 */
/* req->src: [AAD | 평문]
* req->dst: [AAD | 암호문 | 인증태그]
* req->assoclen: AAD 길이
* req->cryptlen: 평문 길이
*/
return 0;
}
static int my_aead_decrypt(struct aead_request *req)
{
/* 1단계: AAD + 암호문에 대해 MAC 재계산
* 2단계: 수신된 인증 태그와 비교
* 3단계: 일치하면 복호화, 불일치시 -EBADMSG 반환
*
* req->src: [AAD | 암호문 | 인증태그]
* req->cryptlen: 암호문 + 인증태그 길이
* 실제 암호문 길이 = req->cryptlen - ctx->authsize
*/
return 0;
}
static int my_aead_init_tfm(struct crypto_aead *tfm)
{
struct my_aead_ctx *ctx = crypto_aead_ctx(tfm);
/* 내부 알고리즘 할당 */
ctx->enc_tfm = crypto_alloc_skcipher("cbc(aes)", 0, 0);
if (IS_ERR(ctx->enc_tfm))
return PTR_ERR(ctx->enc_tfm);
ctx->mac_tfm = crypto_alloc_shash("hmac(sha256)", 0, 0);
if (IS_ERR(ctx->mac_tfm)) {
crypto_free_skcipher(ctx->enc_tfm);
return PTR_ERR(ctx->mac_tfm);
}
/* AEAD 요청 크기 설정: 내부 skcipher 요청도 포함 */
crypto_aead_set_reqsize(tfm,
sizeof(struct skcipher_request) +
crypto_skcipher_reqsize(ctx->enc_tfm));
return 0;
}
static void my_aead_exit_tfm(struct crypto_aead *tfm)
{
struct my_aead_ctx *ctx = crypto_aead_ctx(tfm);
crypto_free_skcipher(ctx->enc_tfm);
crypto_free_shash(ctx->mac_tfm);
}
static struct aead_alg my_aead_alg = {
.setkey = my_aead_setkey,
.setauthsize = my_aead_setauthsize,
.encrypt = my_aead_encrypt,
.decrypt = my_aead_decrypt,
.init = my_aead_init_tfm,
.exit = my_aead_exit_tfm,
.ivsize = 16,
.maxauthsize = 16,
.chunksize = 16,
.base = {
.cra_name = "authenc(hmac(sha256),cbc(aes))",
.cra_driver_name = "my-authenc-hmac-sha256-cbc-aes",
.cra_priority = 100,
.cra_flags = CRYPTO_ALG_ASYNC,
.cra_blocksize = 1,
.cra_ctxsize = sizeof(struct my_aead_ctx),
.cra_module = THIS_MODULE,
}
};
AEAD의 scatterlist는 [AAD | 데이터 | 인증태그] 순서입니다. 암호화 시 req->cryptlen은 평문 길이이고 출력에 인증태그가 추가됩니다. 복호화 시 req->cryptlen은 암호문 + 인증태그 길이이므로, 실제 암호문 길이는 req->cryptlen - authsize입니다. 이 비대칭적 규약을 잘못 구현하면 데이터 손상이 발생합니다.
AEAD 하드웨어 드라이버 구현: aead_engine_alg 기반 GCM 예시
위 예시는 "기존 암호 + MAC 조합"을 직접 보여 주기 위한 것입니다. 하지만 실제 SoC/PCIe 가속기는 대개 GCM/CCM tag 생성과 검증을 한 번에 처리합니다. 이런 장치를 driver로 올릴 때는 aead_engine_alg와 crypto_transfer_aead_request_to_engine() 패턴이 가장 실용적입니다. 핵심은 요청 레이아웃을 그대로 하드웨어 명령으로 번역하고, 하드웨어가 못 받는 입력만 fallback으로 보내는 것입니다.
#include <crypto/internal/aead.h>
#include <crypto/engine.h>
struct my_gcm_dev {
struct device *dev;
struct crypto_engine *engine;
struct aead_request *active_req;
};
struct my_gcm_tfm_ctx {
struct my_gcm_dev *dd;
struct crypto_aead *fallback;
unsigned int authsize;
u8 key[32];
unsigned int keylen;
};
struct my_gcm_reqctx {
dma_addr_t src_dma;
dma_addr_t dst_dma;
u8 iv[16];
bool decrypt;
bool mapped;
};
static inline struct aead_request *my_gcm_fallback_req(struct my_gcm_reqctx *rctx)
{
return PTR_ALIGN(rctx + 1, crypto_tfm_ctx_alignment());
}
static int my_gcm_setkey(struct crypto_aead *tfm,
const u8 *key, unsigned int keylen)
{
struct my_gcm_tfm_ctx *ctx = crypto_aead_ctx(tfm);
if (keylen != 16 && keylen != 24 && keylen != 32)
return -EINVAL;
memcpy(ctx->key, key, keylen);
ctx->keylen = keylen;
return crypto_aead_setkey(ctx->fallback, key, keylen);
}
static int my_gcm_setauthsize(struct crypto_aead *tfm,
unsigned int authsize)
{
struct my_gcm_tfm_ctx *ctx = crypto_aead_ctx(tfm);
switch (authsize) {
case 8:
case 12:
case 16:
ctx->authsize = authsize;
return crypto_aead_setauthsize(ctx->fallback, authsize);
}
return -EINVAL;
}
static int my_gcm_init_tfm(struct crypto_aead *tfm)
{
struct my_gcm_tfm_ctx *ctx = crypto_aead_ctx(tfm);
ctx->fallback = crypto_alloc_aead("gcm(aes-generic)", 0, 0);
if (IS_ERR(ctx->fallback))
return PTR_ERR(ctx->fallback);
ctx->authsize = 16;
crypto_aead_setauthsize(ctx->fallback, ctx->authsize);
crypto_aead_set_reqsize_dma(tfm,
sizeof(struct my_gcm_reqctx) +
crypto_tfm_ctx_alignment() +
sizeof(struct aead_request) +
crypto_aead_reqsize(ctx->fallback));
return 0;
}
static void my_gcm_exit_tfm(struct crypto_aead *tfm)
{
struct my_gcm_tfm_ctx *ctx = crypto_aead_ctx(tfm);
crypto_free_aead(ctx->fallback);
}
static int my_gcm_do_fallback(struct aead_request *req, bool decrypt)
{
struct crypto_aead *tfm = crypto_aead_reqtfm(req);
struct my_gcm_tfm_ctx *ctx = crypto_aead_ctx(tfm);
struct my_gcm_reqctx *rctx = aead_request_ctx(req);
struct aead_request *subreq = my_gcm_fallback_req(rctx);
aead_request_set_tfm(subreq, ctx->fallback);
aead_request_set_callback(subreq, req->base.flags,
req->base.complete, req->base.data);
aead_request_set_crypt(subreq, req->src, req->dst,
req->cryptlen, req->iv);
aead_request_set_ad(subreq, req->assoclen);
if (decrypt)
return crypto_aead_decrypt(subreq);
return crypto_aead_encrypt(subreq);
}
static int my_gcm_encrypt(struct aead_request *req)
{
struct crypto_aead *tfm = crypto_aead_reqtfm(req);
struct my_gcm_tfm_ctx *ctx = crypto_aead_ctx(tfm);
struct my_gcm_reqctx *rctx = aead_request_ctx(req);
rctx->decrypt = false;
if (!my_gcm_hw_can_handle(req))
return my_gcm_do_fallback(req, false);
return crypto_transfer_aead_request_to_engine(ctx->dd->engine, req);
}
static int my_gcm_decrypt(struct aead_request *req)
{
struct crypto_aead *tfm = crypto_aead_reqtfm(req);
struct my_gcm_tfm_ctx *ctx = crypto_aead_ctx(tfm);
struct my_gcm_reqctx *rctx = aead_request_ctx(req);
rctx->decrypt = true;
if (!my_gcm_hw_can_handle(req))
return my_gcm_do_fallback(req, true);
return crypto_transfer_aead_request_to_engine(ctx->dd->engine, req);
}
static int my_gcm_do_one_request(struct crypto_engine *engine, void *areq)
{
struct crypto_async_request *base = areq;
struct aead_request *req = container_of(base, struct aead_request, base);
struct crypto_aead *tfm = crypto_aead_reqtfm(req);
struct my_gcm_tfm_ctx *ctx = crypto_aead_ctx(tfm);
struct my_gcm_reqctx *rctx = aead_request_ctx_dma(req);
(void)engine;
ctx->dd->active_req = req;
memcpy(rctx->iv, req->iv, crypto_aead_ivsize(tfm));
my_gcm_map_req(ctx->dd, req, rctx);
my_gcm_program(ctx->dd, req->assoclen, req->cryptlen,
crypto_aead_authsize(tfm), rctx->iv, rctx->decrypt);
my_gcm_kick(ctx->dd);
return -EINPROGRESS;
}
static irqreturn_t my_gcm_irq(int irq, void *data)
{
struct my_gcm_dev *dd = data;
struct aead_request *req = dd->active_req;
struct crypto_aead *tfm = crypto_aead_reqtfm(req);
struct my_gcm_reqctx *rctx = aead_request_ctx_dma(req);
int err = my_gcm_status(dd);
(void)irq;
(void)tfm;
if (!err && rctx->decrypt && !my_gcm_tag_ok(dd))
err = -EBADMSG;
if (rctx->mapped)
my_gcm_unmap_req(dd, req, rctx);
dd->active_req = NULL;
crypto_finalize_aead_request(dd->engine, req, err);
return IRQ_HANDLED;
}
static struct aead_engine_alg my_gcm_alg = {
.base = {
.setkey = my_gcm_setkey,
.setauthsize = my_gcm_setauthsize,
.encrypt = my_gcm_encrypt,
.decrypt = my_gcm_decrypt,
.init = my_gcm_init_tfm,
.exit = my_gcm_exit_tfm,
.ivsize = 12,
.maxauthsize = 16,
.chunksize = 1,
.base = {
.cra_name = "gcm(aes)",
.cra_driver_name = "gcm-aes-myhw",
.cra_priority = 450,
.cra_flags = CRYPTO_ALG_ASYNC | CRYPTO_ALG_NEED_FALLBACK,
.cra_blocksize = 1,
.cra_ctxsize = sizeof(struct my_gcm_tfm_ctx),
.cra_alignmask = 0xf,
.cra_module = THIS_MODULE,
},
},
.op = {
.do_one_request = my_gcm_do_one_request,
},
};
| 구현 항목 | driver가 반드시 맞춰야 하는 의미 |
|---|---|
setauthsize() | 하드웨어가 지원하는 tag 길이만 허용해야 합니다. 허용 범위를 넓게 잡아 두고 내부에서 임의 절단하면 FIPS, IPsec, TLS 상호운용성이 모두 깨집니다. |
req->assoclen | AAD 길이입니다. AAD는 암호화되지 않지만 tag 계산에는 반드시 포함됩니다. 일부 H/W는 이를 별도 length register로 요구합니다. |
req->cryptlen | 암호화에서는 평문 길이, 복호화에서는 암호문 + tag 길이입니다. decryption 경로에서 tag 길이를 따로 빼지 않으면 DMA length를 잘못 프로그래밍합니다. |
-EBADMSG | 복호화 tag 검증 실패는 일반 I/O 오류가 아니라 반드시 -EBADMSG로 내야 합니다. 상위 계층은 이를 "인증 실패"로 구분합니다. |
| fallback 동기화 | fallback tfm에도 같은 key와 authsize가 들어 있어야 합니다. 그렇지 않으면 특정 입력에서만 결과가 달라지는 가장 나쁜 종류의 버그가 생깁니다. |
[AAD][payload][tag]를 한 덩어리로 받는다고 해서
커널 요청도 그렇게 단순하다고 생각하면 안 됩니다. src와 dst는 동일하거나 다를 수 있고, AAD는 out-of-place에서도 목적지 SG에 "공간은 있지만 실제로는 복사하지 않는" 구간이 될 수 있습니다.
따라서 SG를 단순 linear 버퍼처럼 다루지 말고, 하드웨어가 꼭 contiguous를 요구하면 그때만 bounce buffer로 내리는 편이 안전합니다.
템플릿(Template) 구현
템플릿은 기존 알고리즘을 래핑하여 새로운 알고리즘을 자동 생성하는 메커니즘입니다. 예를 들어 cbc 템플릿은 임의의 블록 암호를 CBC 모드로 래핑합니다:
#include <crypto/internal/skcipher.h>
/* 템플릿 인스턴스의 컨텍스트 */
struct my_tmpl_instance_ctx {
struct crypto_skcipher_spawn spawn; /* 내부 알고리즘 참조 */
};
struct my_tmpl_tfm_ctx {
struct crypto_skcipher *child; /* 실제 내부 tfm */
};
/* 템플릿 인스턴스 생성: "mymode(aes)" 요청 시 호출 */
static int my_tmpl_create(struct crypto_template *tmpl,
struct rtattr **tb)
{
struct skcipher_instance *inst;
struct my_tmpl_instance_ctx *ictx;
struct skcipher_alg_common *alg;
u32 mask;
int err;
/* 내부 알고리즘 이름 추출 (예: "aes" from "mymode(aes)") */
err = crypto_check_attr_type(tb, CRYPTO_ALG_TYPE_SKCIPHER, &mask);
if (err)
return err;
/* 인스턴스 할당 */
inst = kzalloc(sizeof(*inst) + sizeof(*ictx), GFP_KERNEL);
if (!inst)
return -ENOMEM;
ictx = skcipher_instance_ctx(inst);
/* 내부 알고리즘(spawn) 바인딩 */
err = crypto_grab_skcipher(&ictx->spawn,
skcipher_crypto_instance(inst),
crypto_attr_alg_name(tb[1]),
0, mask);
if (err)
goto err_free;
alg = crypto_spawn_skcipher_alg_common(&ictx->spawn);
/* 인스턴스 이름 조합: "mymode(inner_alg_name)" */
err = snprintf(inst->alg.base.cra_name, CRYPTO_MAX_ALG_NAME,
"mymode(%s)", alg->base.cra_name);
err = snprintf(inst->alg.base.cra_driver_name, CRYPTO_MAX_ALG_NAME,
"mymode(%s)", alg->base.cra_driver_name);
/* 내부 알고리즘 속성 상속 */
inst->alg.base.cra_priority = alg->base.cra_priority;
inst->alg.base.cra_blocksize = alg->base.cra_blocksize;
inst->alg.base.cra_alignmask = alg->base.cra_alignmask;
inst->alg.base.cra_ctxsize = sizeof(struct my_tmpl_tfm_ctx);
inst->alg.ivsize = alg->ivsize;
inst->alg.min_keysize = alg->min_keysize;
inst->alg.max_keysize = alg->max_keysize;
/* 콜백 등록 */
inst->alg.setkey = my_tmpl_setkey;
inst->alg.encrypt = my_tmpl_encrypt;
inst->alg.decrypt = my_tmpl_decrypt;
inst->alg.init = my_tmpl_init_tfm;
inst->alg.exit = my_tmpl_exit_tfm;
inst->free = my_tmpl_free_instance;
/* 인스턴스 등록 */
err = skcipher_register_instance(tmpl, inst);
if (err)
goto err_free;
return 0;
err_free:
kfree(inst);
return err;
}
/* 템플릿 등록 */
static struct crypto_template my_tmpl = {
.name = "mymode",
.create = my_tmpl_create,
.module = THIS_MODULE,
};
static int __init my_tmpl_mod_init(void)
{
return crypto_register_template(&my_tmpl);
}
static void __exit my_tmpl_mod_exit(void)
{
crypto_unregister_template(&my_tmpl);
}
Spawn 메커니즘: crypto_spawn은 템플릿이 내부 알고리즘의 생명주기를 추적하도록 합니다. 내부 알고리즘이 해제되면 이를 사용하는 모든 템플릿 인스턴스도 자동으로 무효화(Invalidation)됩니다. 이로써 모듈 unload 시 dangling reference를 방지합니다.
드라이버 작성자의 핵심 감각: tfm 컨텍스트와 request 컨텍스트를 분리하라
Crypto API 제공자 입장에서 가장 중요한 설계는 무엇이 tfm에 속하고 무엇이 request에 속하는가입니다. 같은 알고리즘 인스턴스를 여러 CPU가 동시에 사용할 수 있으므로, cra_ctxsize로 잡은 tfm 컨텍스트에는 키 스케줄, 디바이스 포인터, fallback tfm, 정적 capability 같은 장수 상태만 두어야 합니다. 반대로 DMA mapping, bounce buffer, SG walk 상태, IRQ 완료용 descriptor는 요청마다 독립적이어야 하므로 reqsize에 들어가야 합니다.
#include <crypto/internal/skcipher.h>
#include <crypto/internal/hash.h>
struct my_dev {
struct device *dev;
void __iomem *regs;
int irq;
};
/* tfm당 한 번만 살아 있는 상태 */
struct my_skcipher_tfm_ctx {
struct my_dev *dd;
struct crypto_skcipher *fallback;
u32 round_keys[60];
unsigned int keylen;
};
/* 요청마다 새로 필요한 상태 */
struct my_skcipher_reqctx {
dma_addr_t src_dma;
dma_addr_t dst_dma;
u8 iv[16];
bool mapped;
bool use_fallback;
};
static int my_skcipher_init_tfm(struct crypto_skcipher *tfm)
{
struct my_skcipher_tfm_ctx *ctx = crypto_skcipher_ctx(tfm);
ctx->fallback = crypto_alloc_skcipher("cbc(aes-generic)", 0, 0);
if (IS_ERR(ctx->fallback))
return PTR_ERR(ctx->fallback);
/* DMA 정렬까지 고려한 request private area */
crypto_skcipher_set_reqsize_dma(tfm, sizeof(struct my_skcipher_reqctx));
return 0;
}
static void my_skcipher_exit_tfm(struct crypto_skcipher *tfm)
{
struct my_skcipher_tfm_ctx *ctx = crypto_skcipher_ctx(tfm);
crypto_free_skcipher(ctx->fallback);
}
static int my_ahash_init_tfm(struct crypto_ahash *tfm)
{
/* 해시도 동일한 패턴: statesize와 reqsize를 정직하게 설정 */
crypto_ahash_set_statesize(tfm, 32);
crypto_ahash_set_reqsize_dma(tfm, 128);
return 0;
}
| 저장 위치 | 무엇을 넣어야 하나 | 무엇을 넣으면 안 되나 |
|---|---|---|
cra_ctxsize로 확보한 tfm ctx | 키 스케줄, fallback tfm, 디바이스 핸들, capability, queue 선택 정보 | 진행 중 DMA 주소, 현재 요청 포인터, 이번 요청용 IV/nonce |
reqsize로 확보한 request ctx | DMA mapping, descriptor, bounce buffer, 완료 상태, per-request IV 복사본 | 공유 키, 장수 전역 상태, 다른 요청과 공유되는 포인터 |
statesize (해시) | export/import 가능한 중간 hash 상태 | 하드웨어 전용 임시 메타데이터 전체를 그대로 덤프(Dump)하는 것 |
update()는 같은 transformation object에 대해 병렬 호출될 수 있으므로, tfm ctx를 "현재 진행 중 해시 상태"처럼 사용하면 레이스가 납니다.
드라이버 메타데이터를 대충 정하면 생기는 문제
Crypto driver는 단순히 함수 포인터만 채우는 구조가 아닙니다. blocksize, alignmask, chunksize, walksize, reqsize 같은 메타데이터가 상위 계층의 메모리 배치, bounce buffer 사용, page 경계 처리, zero-copy 가능 여부를 결정합니다.
| 필드 | 드라이버 작성 기준 | 잘못 잡으면 생기는 일 |
|---|---|---|
cra_blocksize | 수학적/프로토콜적 처리 단위입니다. DMA burst 크기가 아닙니다. | 상위 계층이 잘못된 길이 제약을 가정해 부분 블록 처리에서 망가집니다. |
cra_alignmask | 하드웨어가 실제로 요구하는 정렬을 솔직하게 적어야 합니다. | 너무 낮게 적으면 하드웨어 fault, 너무 높게 적으면 불필요한 realign과 복사가 증가합니다. |
chunksize | 알고리즘이 의미 있게 처리할 수 있는 최소 chunk 단위입니다. | skcipher walk가 비효율적으로 쪼개지거나, page 경계에서 fallback이 과도하게 발생합니다. |
walksize | skcipher_walk에 적합한 처리 크기입니다. 디바이스가 한 번에 소비하기 좋은 단위와 맞추는 경우가 많습니다. | 작게 잡으면 loop overhead가 커지고, 너무 크게 잡으면 bounce/re-align 확률이 올라갑니다. |
reqsize | 요청당 필요한 descriptor, DMA 주소, 임시 상태만 넣습니다. | 부족하면 overflow, 과하면 요청 객체가 비대해지고 캐시 locality가 나빠집니다. |
CRYPTO_ALG_ALLOCATES_MEMORY | 요청 처리 중 메모리 할당이 실제로 일어나면 반드시 반영해야 합니다. | 할당이 없다고 믿은 상위 계층이 atomic 경로에서 알고리즘을 사용하다가 경고나 실패를 맞습니다. |
include/linux/crypto.h는 alignmask와 page 경계 조건을 만족하지 못하면
Crypto API가 내부 realign/임시 버퍼를 만들 수 있음을 명시합니다. 따라서 "왜 H/W driver가 빠르지 않지?"라는 문제는 종종 알고리즘 수학이 아니라 메타데이터 부정확성에서 시작합니다.
ahash 드라이버 구현: DMA 해시 엔진 + 소프트웨어 fallback
해시 가속기는 실무에서 가장 다양한 제약을 가집니다. 어떤 엔진은 진짜 스트리밍(init/update/final)을 지원하고, 어떤 엔진은 digest one-shot만 빠르며, 어떤 엔진은 finup는 되지만 중간 상태 export/import는 안 됩니다. 따라서 ahash driver는 하드웨어가 지원하는 가장 강한 연산을 기준으로 구현하고, 그보다 풍부한 API 표면은 소프트웨어 fallback으로 메워야 합니다.
#include <crypto/internal/hash.h>
#include <crypto/engine.h>
#include <linux/dma-mapping.h>
struct my_sha_dev {
struct device *dev;
struct crypto_engine *engine;
struct ahash_request *active_req;
};
struct my_sha_tfm_ctx {
struct my_sha_dev *dd;
struct crypto_shash *fallback;
};
struct my_sha_reqctx {
dma_addr_t src_dma;
dma_addr_t result_dma;
u8 digest[32];
bool mapped;
};
static int my_sha_init_tfm(struct crypto_ahash *tfm)
{
struct my_sha_tfm_ctx *ctx = crypto_ahash_ctx(tfm);
ctx->fallback = crypto_alloc_shash("sha256-generic", 0, 0);
if (IS_ERR(ctx->fallback))
return PTR_ERR(ctx->fallback);
crypto_ahash_set_statesize(tfm, 32);
crypto_ahash_set_reqsize_dma(tfm, sizeof(struct my_sha_reqctx));
return 0;
}
static void my_sha_exit_tfm(struct crypto_ahash *tfm)
{
struct my_sha_tfm_ctx *ctx = crypto_ahash_ctx(tfm);
crypto_free_shash(ctx->fallback);
}
static int my_sha_init(struct ahash_request *req)
{
struct my_sha_reqctx *rctx = ahash_request_ctx(req);
memset(rctx, 0, sizeof(*rctx));
return 0;
}
static int my_sha_fallback_digest(struct ahash_request *req)
{
struct crypto_ahash *tfm = crypto_ahash_reqtfm(req);
struct my_sha_tfm_ctx *ctx = crypto_ahash_ctx(tfm);
SHASH_DESC_ON_STACK(desc, ctx->fallback);
desc->tfm = ctx->fallback;
return shash_ahash_digest(req, desc);
}
static int my_sha_digest(struct ahash_request *req)
{
struct crypto_ahash *tfm = crypto_ahash_reqtfm(req);
struct my_sha_tfm_ctx *ctx = crypto_ahash_ctx(tfm);
/* 하드웨어가 요구하는 SG 조건을 만족하지 않으면 fallback */
if (!my_sha_hw_can_digest(req->src, req->nbytes))
return my_sha_fallback_digest(req);
return crypto_transfer_hash_request_to_engine(ctx->dd->engine, req);
}
static int my_sha_update(struct ahash_request *req)
{
/* 이 예시는 one-shot 하드웨어를 가정하므로 진짜 스트리밍은 fallback 처리 */
return my_sha_fallback_digest(req);
}
static int my_sha_final(struct ahash_request *req)
{
return my_sha_fallback_digest(req);
}
static int my_sha_do_one_request(struct crypto_engine *engine, void *areq)
{
struct crypto_async_request *base = areq;
struct ahash_request *req = ahash_request_cast(base);
struct crypto_ahash *tfm = crypto_ahash_reqtfm(req);
struct my_sha_tfm_ctx *ctx = crypto_ahash_ctx(tfm);
struct my_sha_reqctx *rctx = ahash_request_ctx_dma(req);
(void)engine;
ctx->dd->active_req = req;
rctx->src_dma = dma_map_sgtable(ctx->dd->dev, my_req_to_sgt(req), DMA_TO_DEVICE, 0);
rctx->result_dma = dma_map_single(ctx->dd->dev, rctx->digest, 32, DMA_FROM_DEVICE);
rctx->mapped = true;
/* 레지스터 적재 후 SHA 엔진 시작 */
my_sha_hw_kick(ctx->dd, req, rctx);
return -EINPROGRESS;
}
static irqreturn_t my_sha_irq(int irq, void *data)
{
struct my_sha_dev *dd = data;
struct ahash_request *req = dd->active_req;
struct my_sha_reqctx *rctx = ahash_request_ctx_dma(req);
int err = my_sha_hw_status(dd);
if (!err)
memcpy(req->result, rctx->digest, 32);
if (rctx->mapped)
my_sha_unmap_req(dd, req, rctx);
dd->active_req = NULL;
crypto_finalize_hash_request(dd->engine, req, err);
return IRQ_HANDLED;
}
static struct ahash_engine_alg my_sha_alg = {
.base = {
.init = my_sha_init,
.update = my_sha_update,
.final = my_sha_final,
.digest = my_sha_digest,
.init_tfm = my_sha_init_tfm,
.exit_tfm = my_sha_exit_tfm,
.reqsize = sizeof(struct my_sha_reqctx),
.halg = {
.digestsize = 32,
.statesize = 32,
.base = {
.cra_name = "sha256",
.cra_driver_name = "sha256-myhw",
.cra_priority = 300,
.cra_flags = CRYPTO_ALG_ASYNC | CRYPTO_ALG_KERN_DRIVER_ONLY,
.cra_blocksize = 64,
.cra_ctxsize = sizeof(struct my_sha_tfm_ctx),
.cra_alignmask = 0,
.cra_module = THIS_MODULE,
},
},
},
.op = {
.do_one_request = my_sha_do_one_request,
},
};
| 하드웨어 능력 | 드라이버 구현 전략 |
|---|---|
| 진짜 스트리밍 가능 | init/update/final/export/import를 하드웨어 상태 기계에 맞게 직접 구현합니다. |
finup까지만 가능 | update()는 reqctx에 staging하고, 마지막 final() 또는 finup()에서 실제 장치에 태웁니다. |
one-shot digest만 빠름 | digest()는 H/W, 나머지는 shash fallback으로 보내는 혼합형이 가장 현실적입니다. |
update()는 같은 tfm에 대해 병렬 호출될 수 있으므로
"현재 해시 상태"를 tfm ctx에 저장하면 안 됩니다. 중간 상태는 request ctx나 export/import 버퍼에 있어야 합니다.
RNG 드라이버 구현: TRNG와 DRBG 래퍼를 구분하라
rng_alg는 단순해 보이지만 정책적 의미가 큽니다. 하드웨어 TRNG를 그대로 노출하는지, 내부적으로 DRBG를 돌리는지, 외부에서 seed를 넣어야 하는지에 따라 seed()와 seedsize 의미가 달라집니다. 특히 FIPS 경계 안에서는 "엔트로피를 내는 장치"와 "결정론적으로 바이트를 내는 DRBG"를 혼동하면 안 됩니다.
#include <crypto/rng.h>
#include <crypto/internal/rng.h>
struct my_trng_ctx {
struct my_dev *dd;
bool seeded;
u8 personalization[48];
};
static inline struct my_trng_ctx *my_rng_ctx(struct crypto_rng *tfm)
{
return crypto_tfm_ctx(crypto_rng_tfm(tfm));
}
static int my_trng_seed(struct crypto_rng *tfm,
const u8 *seed, unsigned int slen)
{
struct my_trng_ctx *ctx = my_rng_ctx(tfm);
if (slen != 48)
return -EINVAL;
memcpy(ctx->personalization, seed, slen);
ctx->seeded = true;
return 0;
}
static int my_trng_generate(struct crypto_rng *tfm,
const u8 *src, unsigned int slen,
u8 *dst, unsigned int dlen)
{
struct my_trng_ctx *ctx = my_rng_ctx(tfm);
if (!ctx->seeded)
return -EINVAL;
while (dlen) {
u32 word;
if (!my_hw_rng_ready(ctx->dd))
return -EAGAIN;
word = my_hw_rng_read(ctx->dd);
memcpy(dst, &word, min(dlen, sizeof(word)));
dst += min(dlen, sizeof(word));
dlen -= min(dlen, sizeof(word));
}
if (src && slen)
my_hw_mix_additional_input(ctx->dd, src, slen);
return 0;
}
static struct rng_alg my_trng_alg = {
.generate = my_trng_generate,
.seed = my_trng_seed,
.seedsize = 48,
.base = {
.cra_name = "mytrng",
.cra_driver_name = "mytrng-hw",
.cra_priority = 200,
.cra_ctxsize = sizeof(struct my_trng_ctx),
.cra_module = THIS_MODULE,
},
};
| 유형 | seedsize 설계 | seed() 의미 |
|---|---|---|
| 하드웨어 TRNG | 보통 0 또는 정책상 필요한 personalization 길이 | 대개 optional personalization 또는 내부 mixing 트리거 |
| H/W + DRBG 하이브리드 | DRBG가 요구하는 seed 길이 | DRBG 상태 재초기화 |
| 순수 DRBG 구현 | 필수 seed 길이를 정확히 명시 | 새 내부 상태를 만드는 핵심 콜백 |
seedsize를 애매하게 잡거나, 반대로 외부 시드가 꼭 필요한데 seedsize = 0으로 등록하는 경우입니다.
이 값은 소비자가 crypto_rng_reset()을 언제 불러야 하는지 결정하므로, 정책적으로 정확해야 합니다.
압축 드라이버 구현: acomp는 engine helper가 없는 점이 핵심
acomp는 비동기 압축 API이지만, skcipher/aead/ahash처럼 crypto_engine 래퍼가 제공되지는 않습니다. 따라서 비동기 압축 드라이버는 자체 queue/workqueue/IRQ 경로를 직접 관리하는 경우가 많습니다. 이것이 암호 드라이버와 압축 드라이버의 큰 차이점입니다.
#include <crypto/internal/acompress.h>
#include <linux/workqueue.h>
struct my_zip_tfm_ctx {
struct my_dev *dd;
};
struct my_zip_reqctx {
struct work_struct work;
struct acomp_req *req;
bool allocated_dst;
};
static void my_zip_dst_free(struct scatterlist *dst)
{
my_sg_free(dst);
}
static void my_zip_do_work(struct work_struct *work)
{
struct my_zip_reqctx *rctx = container_of(work, struct my_zip_reqctx, work);
struct acomp_req *req = rctx->req;
int err;
err = my_zip_hw_run(req);
if (!err)
req->dlen = my_zip_result_len(req);
acomp_request_complete(req, err);
}
static int my_zip_compress(struct acomp_req *req)
{
struct crypto_acomp *tfm = crypto_acomp_reqtfm(req);
struct my_zip_tfm_ctx *ctx = acomp_tfm_ctx(tfm);
struct my_zip_reqctx *rctx = acomp_request_ctx(req);
memset(rctx, 0, sizeof(*rctx));
rctx->req = req;
if (req->flags & CRYPTO_ACOMP_ALLOC_OUTPUT) {
req->dst = my_zip_alloc_output_sg(req->dlen);
if (!req->dst)
return -ENOMEM;
rctx->allocated_dst = true;
}
if (!my_zip_hw_can_handle(ctx->dd, req))
return -EINVAL;
INIT_WORK(&rctx->work, my_zip_do_work);
queue_work(ctx->dd->wq, &rctx->work);
return -EINPROGRESS;
}
static int my_zip_decompress(struct acomp_req *req)
{
/* 압축과 동일한 패턴으로 하드웨어 또는 workqueue 경로 사용 */
return my_zip_compress(req);
}
static int my_zip_init_tfm(struct crypto_acomp *tfm)
{
struct my_zip_tfm_ctx *ctx = acomp_tfm_ctx(tfm);
ctx->dd = my_pick_zip_device();
return 0;
}
static struct acomp_alg my_zip_alg = {
.compress = my_zip_compress,
.decompress = my_zip_decompress,
.dst_free = my_zip_dst_free,
.init = my_zip_init_tfm,
.reqsize = sizeof(struct my_zip_reqctx),
.base = {
.cra_name = "deflate",
.cra_driver_name = "deflate-myzip",
.cra_priority = 250,
.cra_flags = CRYPTO_ALG_ASYNC,
.cra_ctxsize = sizeof(struct my_zip_tfm_ctx),
.cra_module = THIS_MODULE,
},
};
| 포인트 | 설명 |
|---|---|
req->dst == NULL | 소비자가 출력 버퍼 자동 할당을 요청한 경우입니다. 드라이버가 SG를 할당했다면 dst_free로 정리 경로를 제공해야 합니다. |
acomp_request_complete() | 비동기 완료 통지는 이 헬퍼로 수렴시키는 편이 안전합니다. |
| engine helper 부재 | 압축은 crypto_engine_register_*() 계열이 없으므로 자체 queue 설계가 필요합니다. |
AKCIPHER 드라이버 구현: 길이 계약과 키 파싱이 절반입니다
공개키 엔진 드라이버는 수학 연산보다도 키 파싱, 출력 길이 산정, verify 입력 규약을 정확히 맞추는 일이 더 어렵습니다. 특히 akcipher_request는 검증 경로에서 src = signature || digest 형태를 사용하므로, src_len과 dst_len의 의미가 연산마다 달라진다는 점을 기억해야 합니다.
#include <crypto/akcipher.h>
#include <crypto/internal/akcipher.h>
struct my_rsa_tfm_ctx {
struct my_dev *dd;
unsigned int modulus_size;
bool has_pub;
bool has_priv;
};
struct my_rsa_reqctx {
dma_addr_t src_dma;
dma_addr_t dst_dma;
};
static int my_rsa_set_pub_key(struct crypto_akcipher *tfm,
const void *key, unsigned int keylen)
{
struct my_rsa_tfm_ctx *ctx = akcipher_tfm_ctx(tfm);
/* BER/DER 파싱 후 modulus 길이 계산 */
ctx->modulus_size = my_parse_rsa_pubkey(key, keylen, ctx);
if (!ctx->modulus_size)
return -EINVAL;
ctx->has_pub = true;
return 0;
}
static int my_rsa_set_priv_key(struct crypto_akcipher *tfm,
const void *key, unsigned int keylen)
{
struct my_rsa_tfm_ctx *ctx = akcipher_tfm_ctx(tfm);
ctx->modulus_size = my_parse_rsa_privkey(key, keylen, ctx);
if (!ctx->modulus_size)
return -EINVAL;
ctx->has_priv = true;
return 0;
}
static unsigned int my_rsa_max_size(struct crypto_akcipher *tfm)
{
struct my_rsa_tfm_ctx *ctx = akcipher_tfm_ctx(tfm);
return ctx->modulus_size;
}
static int my_rsa_encrypt(struct akcipher_request *req)
{
struct crypto_akcipher *tfm = crypto_akcipher_reqtfm(req);
struct my_rsa_tfm_ctx *ctx = akcipher_tfm_ctx(tfm);
if (!ctx->has_pub)
return -EINVAL;
if (req->dst_len < ctx->modulus_size) {
req->dst_len = ctx->modulus_size;
return -EOVERFLOW;
}
/* PKCS#1/OAEP padding 검증 후 장치 또는 소프트웨어 경로 수행 */
req->dst_len = ctx->modulus_size;
return my_rsa_hw_encrypt(req, ctx);
}
static int my_rsa_verify(struct akcipher_request *req)
{
struct crypto_akcipher *tfm = crypto_akcipher_reqtfm(req);
struct my_rsa_tfm_ctx *ctx = akcipher_tfm_ctx(tfm);
/* verify 입력 규약:
* req->src = [signature][digest]
* req->src_len = signature 길이
* req->dst_len = digest 길이
*/
if (req->src_len != ctx->modulus_size)
return -EINVAL;
if (!my_rsa_sig_matches_digest(req, ctx))
return -EBADMSG;
return 0;
}
static int my_rsa_init_tfm(struct crypto_akcipher *tfm)
{
akcipher_set_reqsize_dma(tfm, sizeof(struct my_rsa_reqctx));
return 0;
}
static struct akcipher_alg my_rsa_alg = {
.encrypt = my_rsa_encrypt,
.verify = my_rsa_verify,
.set_pub_key = my_rsa_set_pub_key,
.set_priv_key = my_rsa_set_priv_key,
.max_size = my_rsa_max_size,
.init = my_rsa_init_tfm,
.base = {
.cra_name = "rsa",
.cra_driver_name = "rsa-myhw",
.cra_priority = 500,
.cra_flags = CRYPTO_ALG_ASYNC,
.cra_ctxsize = sizeof(struct my_rsa_tfm_ctx),
.cra_module = THIS_MODULE,
},
};
| 계약 | 드라이버가 반드시 지킬 것 |
|---|---|
max_size() | 현재 설정된 키 기준으로 충분한 출력 길이를 반환해야 합니다. |
| 출력 버퍼 부족 | req->dst_len을 필요한 길이로 갱신하고 오류를 반환해야 합니다. |
| verify 입력 형식 | src = signature || digest, src_len = signature, dst_len = digest 규약을 지켜야 합니다. |
| 키 설정 실패 | 부분적으로 파싱한 키 상태를 남기지 말고, 다음 setkey 호출에서 일관되게 다시 시작해야 합니다. |
KPP 드라이버 구현: 공개키 생성과 공유 비밀 계산을 분리하라
KPP 드라이버는 "세션 키 합의"라는 높은 수준 개념 대신, 훨씬 더 낮은 두 primitive만 제공합니다. 하나는 내 공개키 생성, 다른 하나는 상대 공개키를 입력으로 공유 비밀 계산입니다. 소비자(TLS, IPsec, WireGuard 전단 등)는 그 결과를 다시 KDF에 넣어 실제 데이터 키를 만듭니다.
#include <crypto/kpp.h>
#include <crypto/internal/kpp.h>
#include <crypto/ecdh.h>
struct my_ecdh_tfm_ctx {
struct my_dev *dd;
u8 priv[80];
unsigned int priv_len;
unsigned int pub_len;
unsigned int secret_len;
};
struct my_ecdh_reqctx {
dma_addr_t src_dma;
dma_addr_t dst_dma;
};
static int my_ecdh_set_secret(struct crypto_kpp *tfm,
const void *buffer, unsigned int len)
{
struct my_ecdh_tfm_ctx *ctx = kpp_tfm_ctx(tfm);
struct ecdh params;
int ret;
ret = crypto_ecdh_decode_key(buffer, len, ¶ms);
if (ret)
return ret;
if (params.key_size > sizeof(ctx->priv))
return -EINVAL;
memcpy(ctx->priv, params.key, params.key_size);
ctx->priv_len = params.key_size;
ctx->pub_len = my_ecdh_public_size(params.key_size);
ctx->secret_len = my_ecdh_secret_size(params.key_size);
return 0;
}
static unsigned int my_ecdh_max_size(struct crypto_kpp *tfm)
{
struct my_ecdh_tfm_ctx *ctx = kpp_tfm_ctx(tfm);
return max(ctx->pub_len, ctx->secret_len);
}
static int my_ecdh_generate_public_key(struct kpp_request *req)
{
struct crypto_kpp *tfm = crypto_kpp_reqtfm(req);
struct my_ecdh_tfm_ctx *ctx = kpp_tfm_ctx(tfm);
if (req->dst_len < ctx->pub_len) {
req->dst_len = ctx->pub_len;
return -EOVERFLOW;
}
req->dst_len = ctx->pub_len;
return my_ecdh_hw_make_public(req, ctx);
}
static int my_ecdh_compute_shared_secret(struct kpp_request *req)
{
struct crypto_kpp *tfm = crypto_kpp_reqtfm(req);
struct my_ecdh_tfm_ctx *ctx = kpp_tfm_ctx(tfm);
if (req->dst_len < ctx->secret_len) {
req->dst_len = ctx->secret_len;
return -EOVERFLOW;
}
req->dst_len = ctx->secret_len;
return my_ecdh_hw_shared_secret(req, ctx);
}
static int my_ecdh_init_tfm(struct crypto_kpp *tfm)
{
kpp_set_reqsize_dma(tfm, sizeof(struct my_ecdh_reqctx));
return 0;
}
static struct kpp_alg my_ecdh_alg = {
.set_secret = my_ecdh_set_secret,
.generate_public_key = my_ecdh_generate_public_key,
.compute_shared_secret = my_ecdh_compute_shared_secret,
.max_size = my_ecdh_max_size,
.init = my_ecdh_init_tfm,
.base = {
.cra_name = "ecdh",
.cra_driver_name = "ecdh-myhw",
.cra_priority = 350,
.cra_flags = CRYPTO_ALG_ASYNC,
.cra_ctxsize = sizeof(struct my_ecdh_tfm_ctx),
.cra_module = THIS_MODULE,
},
};
crypto_ecdh_encode_key() / crypto_ecdh_decode_key() 같은 helper를 활용하는 편이 안전합니다.
드라이버가 독자 형식을 만들기 시작하면 소비자와의 호환성, test vector, FIPS 경계 설명이 모두 어려워집니다.
하드웨어 드라이버 등록(Driver Registration) 패턴: crypto_engine을 기준으로 보는 전체 수명
하드웨어 crypto driver의 전형적인 뼈대는 다음 순서입니다. probe에서 engine 생성 → 알고리즘 등록 → tfm init에서 per-transform state 준비 → encrypt/decrypt/digest가 engine queue에 요청 위임 → IRQ에서 finalize 입니다. 이 패턴을 지키면 backlog와 완료 통지가 Crypto API 규약에 맞춰 정리됩니다.
#include <crypto/engine.h>
#include <crypto/internal/skcipher.h>
struct my_accel_dev {
struct device *dev;
struct crypto_engine *engine;
};
static int my_hw_encrypt(struct skcipher_request *req)
{
struct crypto_skcipher *tfm = crypto_skcipher_reqtfm(req);
struct my_skcipher_tfm_ctx *ctx = crypto_skcipher_ctx(tfm);
return crypto_transfer_skcipher_request_to_engine(ctx->dd->engine, req);
}
static int my_hw_decrypt(struct skcipher_request *req)
{
return my_hw_encrypt(req);
}
static int my_hw_do_one_request(struct crypto_engine *engine, void *areq)
{
struct crypto_async_request *base = areq;
struct skcipher_request *req = container_of(base, struct skcipher_request, base);
(void)engine;
my_hw_map_req(req);
my_hw_start(req);
return -EINPROGRESS;
}
static struct skcipher_engine_alg my_hw_algs[] = {{
.base = {
.setkey = my_cipher_setkey,
.encrypt = my_hw_encrypt,
.decrypt = my_hw_decrypt,
.init = my_skcipher_init_tfm,
.exit = my_skcipher_exit_tfm,
.min_keysize = 16,
.max_keysize = 32,
.ivsize = 16,
.chunksize = 16,
.walksize = 16,
.base = {
.cra_name = "cbc(aes)",
.cra_driver_name = "cbc-aes-myhw",
.cra_priority = 400,
.cra_flags = CRYPTO_ALG_ASYNC | CRYPTO_ALG_NEED_FALLBACK,
.cra_blocksize = 16,
.cra_ctxsize = sizeof(struct my_skcipher_tfm_ctx),
.cra_alignmask = 0xf,
.cra_module = THIS_MODULE,
},
},
.op = {
.do_one_request = my_hw_do_one_request,
},
}};
static int my_probe(struct platform_device *pdev)
{
struct my_accel_dev *dd;
int err;
dd = devm_kzalloc(&pdev->dev, sizeof(*dd), GFP_KERNEL);
if (!dd)
return -ENOMEM;
dd->dev = &pdev->dev;
dd->engine = crypto_engine_alloc_init(&pdev->dev, true);
if (!dd->engine)
return -ENOMEM;
err = crypto_engine_start(dd->engine);
if (err)
goto err_engine_exit;
err = crypto_engine_register_skciphers(my_hw_algs, ARRAY_SIZE(my_hw_algs));
if (err)
goto err_engine_stop;
return 0;
err_engine_stop:
crypto_engine_stop(dd->engine);
err_engine_exit:
crypto_engine_exit(dd->engine);
return err;
}
static void my_remove(struct platform_device *pdev)
{
struct my_accel_dev *dd = platform_get_drvdata(pdev);
crypto_engine_unregister_skciphers(my_hw_algs, ARRAY_SIZE(my_hw_algs));
crypto_engine_stop(dd->engine);
crypto_engine_exit(dd->engine);
}
| 엔진 래퍼 | 지원 타입 | 완료 헬퍼 |
|---|---|---|
crypto_engine_register_skcipher* | 대칭 암호 | crypto_finalize_skcipher_request() |
crypto_engine_register_aead* | AEAD | crypto_finalize_aead_request() |
crypto_engine_register_ahash* | 해시 | crypto_finalize_hash_request() |
crypto_engine_register_akcipher() | 공개키 | crypto_finalize_akcipher_request() |
crypto_engine_register_kpp() | 키 합의 | crypto_finalize_kpp_request() |
cra_name이 남아 있는 상태에서 engine을 먼저 없애면, 새 tfm 할당 또는 in-flight 요청과 경합할 수 있습니다.
crypto_queue를 직접 쓰는 드라이버: crypto_engine이 맞지 않을 때
crypto_engine은 대부분의 단일 큐 장치에서 최선의 출발점입니다. 하지만 여러 채널을 가진 장치, vendor scheduler가 별도로 있는 장치, 요청을 일괄(batch)로 묶어야 하는 장치에서는 오히려 crypto_queue를 직접 다루는 편이 더 명확할 수 있습니다. 이 경우 핵심은 단 세 가지입니다. (1) enqueue는 락 안에서, (2) backlog 통지는 dequeue 직후, (3) 현재 active request 포인터는 완료 전에만 유효해야 합니다.
#include <crypto/algapi.h>
#include <crypto/internal/hash.h>
struct my_queue_dev {
struct device *dev;
spinlock_t lock;
struct crypto_queue queue;
struct ahash_request *active_req;
bool busy;
};
static int my_queue_digest(struct ahash_request *req)
{
struct crypto_ahash *tfm = crypto_ahash_reqtfm(req);
struct my_queue_tfm_ctx *ctx = crypto_ahash_ctx(tfm);
struct my_queue_dev *dd = ctx->dd;
struct crypto_async_request *async_req, *backlog;
unsigned long flags;
int ret = 0;
spin_lock_irqsave(&dd->lock, flags);
ret = ahash_enqueue_request(&dd->queue, req);
if (dd->busy) {
spin_unlock_irqrestore(&dd->lock, flags);
return ret;
}
backlog = crypto_get_backlog(&dd->queue);
async_req = crypto_dequeue_request(&dd->queue);
if (async_req) {
dd->busy = true;
dd->active_req = ahash_request_cast(async_req);
}
spin_unlock_irqrestore(&dd->lock, flags);
if (!async_req)
return ret;
if (backlog)
crypto_request_complete(backlog, -EINPROGRESS);
ret = my_queue_start_hash(dd, dd->active_req);
if (ret != -EINPROGRESS)
my_queue_finish_one(dd, ret);
return ret ? ret : -EINPROGRESS;
}
static void my_queue_finish_one(struct my_queue_dev *dd, int err)
{
struct crypto_async_request *async_req, *backlog;
struct ahash_request *req;
unsigned long flags;
spin_lock_irqsave(&dd->lock, flags);
req = dd->active_req;
dd->active_req = NULL;
dd->busy = false;
backlog = crypto_get_backlog(&dd->queue);
async_req = crypto_dequeue_request(&dd->queue);
if (async_req) {
dd->busy = true;
dd->active_req = ahash_request_cast(async_req);
}
spin_unlock_irqrestore(&dd->lock, flags);
if (req)
ahash_request_complete(req, err);
if (backlog)
crypto_request_complete(backlog, -EINPROGRESS);
if (dd->active_req)
my_queue_start_hash(dd, dd->active_req);
}
static irqreturn_t my_queue_irq(int irq, void *data)
{
struct my_queue_dev *dd = data;
int err = my_queue_hw_status(dd);
(void)irq;
my_queue_finish_one(dd, err);
return IRQ_HANDLED;
}
static int my_queue_probe(struct platform_device *pdev)
{
struct my_queue_dev *dd = platform_get_drvdata(pdev);
spin_lock_init(&dd->lock);
crypto_init_queue(&dd->queue, 64);
dd->active_req = NULL;
dd->busy = false;
return 0;
}
| 지점 | 왜 중요한가 |
|---|---|
ahash_enqueue_request() 반환값 | 0이면 바로 처리 또는 정상 큐잉, -EBUSY면 backlog로 들어간 뒤 나중에 완료될 수 있습니다. "오류"처럼 보여도 실제로는 수락된 요청일 수 있습니다. |
crypto_get_backlog() | backlog request에게는 -EINPROGRESS 통지를 한 번 보내서 "대기열에 들어갔습니다"는 사실을 알립니다. 이 의미를 지키지 않으면 상위 계층 대기 로직이 꼬입니다. |
active_req 수명 | IRQ 완료 전까지만 유효합니다. 완료 후 즉시 NULL로 내리고 새 dequeue를 해야 중복 완료/UAF를 막을 수 있습니다. |
| 락 범위 | queue 조작과 busy 플래그 갱신은 같은 락으로 묶어야 합니다. 반대로 실제 하드웨어 시작 함수는 가능하면 락 밖에서 수행해 인터럽트(Interrupt) 지연을 줄입니다. |
crypto_engine으로 구현하고,
실제로 (1) 다중 채널 스케줄링, (2) 하드웨어 batch submit, (3) vendor firmware queue와의 1:1 매핑 같은 이유가 있을 때만 수동 queue로 내려가는 편이 유지보수에 유리합니다.
직접 queue를 짤수록 completion 중복, backlog 누락, remove 경로 경합 같은 버그가 늘어납니다.
비동기 드라이버의 반환값 계약
Crypto driver에서 가장 치명적인 버그 중 하나는 반환값과 완료 콜백의 의미를 뒤섞는 것입니다. 호출자 입장에서 "이미 끝났는지", "나중에 콜백이 올지", "버퍼 크기만 다시 잡으면 되는지"를 반환값 하나로 판단하기 때문입니다.
| 반환값 | 의미 | 드라이버가 해야 할 일 |
|---|---|---|
0 | 지금 이 함수 호출 안에서 완료됨 | 추가 completion 호출을 해서는 안 됩니다. |
-EINPROGRESS | 요청을 수락했고 나중에 완료됨 | 반드시 IRQ/workqueue/bottom-half 등에서 *_request_complete() 또는 crypto_finalize_*를 호출해야 합니다. |
-EBUSY | backlog 처리로 나중에 완료될 수 있음 | queue/backlog 의미를 지키고, 결국 완료 통지를 해야 합니다. |
-EOVERFLOW | 출력 버퍼가 작음 | req->dst_len을 필요한 크기로 갱신합니다. |
-EINVAL | 길이, 키, IV, SG 형식, unsupported parameter 오류 | 하드웨어 kick 전에 즉시 실패시키는 편이 좋습니다. |
-EBADMSG | 인증 태그 또는 서명 검증 실패 | 데이터 무결성 실패를 일반 I/O 오류와 구분해 주는 것이 중요합니다. |
0을 반환했으면 나중에 callback을 다시 호출하지 마세요.
둘째, -EINPROGRESS 또는 -EBUSY를 반환했으면 언젠가 반드시 완료 통지가 와야 합니다.
이 둘만 어겨도 상위 계층은 중복 완료, 영원한 대기, UAF 같은 치명적 오류를 겪습니다.
테스트 프레임워크
새 알고리즘을 추가하면 반드시 테스트 벡터를 함께 제공해야 합니다. 커널의 Crypto Framework (Crypto API)는 알고리즘 등록 시 자동으로 자가 테스트를 수행합니다.
testmgr — 알고리즘 자동 검증
crypto/testmgr.c는 등록된 모든 알고리즘에 대해 Known Answer Test(KAT)를 수행합니다. 알고리즘이 crypto_register_*()로 등록되면 cryptomgr가 비동기적으로 테스트를 트리거합니다:
/* crypto/testmgr.c — 테스트 벡터 구조체 */
/* 해시 테스트 벡터 */
struct hash_testvec {
const char *key; /* HMAC 키 (선택) */
const char *plaintext; /* 입력 데이터 */
const char *digest; /* 기대 해시 값 */
unsigned int psize; /* 입력 길이 */
unsigned int ksize; /* 키 길이 */
};
/* 대칭 암호 테스트 벡터 */
struct cipher_testvec {
const char *key; /* 암호 키 */
const char *iv; /* 초기화 벡터 */
const char *ptext; /* 평문 */
const char *ctext; /* 기대 암호문 */
unsigned int klen; /* 키 길이 */
unsigned int len; /* 데이터 길이 */
};
/* AEAD 테스트 벡터 */
struct aead_testvec {
const char *key;
const char *iv;
const char *assoc; /* AAD (추가 인증 데이터) */
const char *ptext; /* 평문 */
const char *ctext; /* 암호문 + 인증태그 */
unsigned int klen;
unsigned int alen; /* AAD 길이 */
unsigned int plen; /* 평문 길이 */
unsigned int clen; /* 암호문+태그 길이 */
};
테스트 벡터 추가 방법
새 알고리즘의 테스트 벡터를 추가하려면 두 파일을 수정합니다:
/* 1단계: crypto/testmgr.h — 테스트 벡터 데이터 정의 */
static const struct hash_testvec myhash256_tv_template[] = {
{ /* 벡터 #1: 빈 입력 */
.plaintext = "",
.psize = 0,
.digest = "\xe3\xb0\xc4\x42...", /* 기대 해시 값 */
},
{ /* 벡터 #2: "abc" */
.plaintext = "abc",
.psize = 3,
.digest = "\xba\x78\x16\xbf...",
},
{ /* 벡터 #3: 긴 입력 (NIST 표준 벡터 등) */
.plaintext = "abcdbcdecdefdefg...",
.psize = 56,
.digest = "\x24\x8d\x6a\x61...",
},
};
/* 2단계: crypto/testmgr.c — 테스트 벡터를 알고리즘에 연결 */
static const struct alg_test_desc alg_test_descs[] = {
/* ... 기존 항목들 (알파벳순 정렬!) ... */
{
.alg = "myhash256",
.test = alg_test_hash, /* 테스트 함수 유형 */
.suite = {
.hash = __VECS(myhash256_tv_template)
}
},
/* ... */
};
alg_test_descs[] 배열은 알고리즘 이름의 알파벳순으로 정렬되어 있어야 합니다. 커널은 이진 탐색으로 테스트 항목을 찾으므로, 정렬이 어긋나면 테스트가 실행되지 않습니다.
tcrypt — 성능 벤치마크
crypto/tcrypt.c는 암호 알고리즘의 성능을 측정하는 커널 모듈(Kernel Module)입니다:
# tcrypt 모듈 로드 — mode 번호로 테스트 유형 선택
modprobe tcrypt mode=200 # SHA-256 속도 테스트
modprobe tcrypt mode=500 # AES-CBC 속도 테스트
modprobe tcrypt mode=300 # AEAD 계열 속도 테스트
# 결과 확인
dmesg | tail -50
# testing speed of async cbc(aes) encryption
# test 0 (128 bit key, 16 byte blocks): 1 operation in 1234 cycles (16 bytes)
# test 1 (128 bit key, 64 byte blocks): 1 operation in 2345 cycles (64 bytes)
# 모든 알고리즘 셀프 테스트 실행
modprobe tcrypt mode=0 # 전체 알고리즘 테스트
# 특정 알고리즘만 테스트 (sec 파라미터로 벤치마크 시간 조절)
modprobe tcrypt mode=500 sec=5 # 5초간 벤치마크
| tcrypt mode | 테스트 대상 |
|---|---|
0 | 모든 알고리즘 자가 테스트 (KAT) |
200~215 | 해시 속도 (SHA-1, SHA-256, MD5, BLAKE2 등) |
300~399 | AEAD 속도 (AES-GCM, ChaCha20-Poly1305 등) |
500~599 | skcipher 속도 (AES-CBC, AES-CTR, AES-XTS 등) |
Kconfig / Makefile 통합
커널 트리에 새 알고리즘을 통합하려면 crypto/ 디렉터리의 Kconfig와 Makefile을 수정합니다:
# crypto/Kconfig — 알고리즘 빌드 옵션 추가
config CRYPTO_MYHASH256
tristate "My Hash 256 algorithm"
select CRYPTO_HASH # 해시 프레임워크 의존성
help
This is an example hash algorithm producing a 256-bit digest.
config CRYPTO_MYCIPHER
tristate "My Block Cipher (CBC mode)"
select CRYPTO_SKCIPHER # skcipher 프레임워크 의존성
select CRYPTO_LIB_AES # AES 라이브러리 의존성 (필요 시)
help
My custom block cipher with CBC mode support.
# H/W 가속 구현인 경우
config CRYPTO_MYCIPHER_X86_64
tristate "My Block Cipher (x86_64/AES-NI 가속)"
depends on X86 && 64BIT
select CRYPTO_MYCIPHER # generic 구현 fallback용
select CRYPTO_SIMD # SIMD 헬퍼 */
help
x86_64 AES-NI accelerated implementation.
주요 Kconfig select 대상
| 의존성 | 설명 | 사용 경우 |
|---|---|---|
CRYPTO_HASH | 해시 프레임워크 (shash/ahash) | 해시 알고리즘 |
CRYPTO_SKCIPHER | 대칭 암호 프레임워크 | skcipher 알고리즘 |
CRYPTO_AEAD | AEAD 프레임워크 | AEAD 알고리즘 |
CRYPTO_SIMD | SIMD 컨텍스트 관리 헬퍼 | SSE/AVX/NEON 사용 구현 |
CRYPTO_LIB_AES | AES 키 스케줄 라이브러리 | AES 기반 구현 |
CRYPTO_LIB_SHA256 | SHA-256 라이브러리 함수 | SHA-256 기반 구현 |
MODULE_ALIAS_CRYPTO 매크로(Macro)
/* 모듈 자동 로딩을 위한 alias 등록 */
MODULE_ALIAS_CRYPTO("myhash256");
MODULE_ALIAS_CRYPTO("myhash256-generic");
/* 커널이 "myhash256" 알고리즘을 요청하면:
* 1. crypto_has_alg("myhash256") → 등록 안 됨
* 2. request_module("crypto-myhash256") 호출
* 3. MODULE_ALIAS_CRYPTO 매칭 → myhash256.ko 자동 로드
* 4. module_init 실행 → crypto_register_shash()
* 5. 이제 crypto_alloc_shash("myhash256") 성공
*/
cryptomgr와 알고리즘 탐색 메커니즘
cryptomgr(Crypto Manager)는 알고리즘 등록/해제/탐색을 관리하는 커널 서브시스템입니다:
crypto_larval — 알고리즘 대기 객체
/* crypto/api.c — 알고리즘 탐색 핵심 로직 (간략화) */
struct crypto_alg *crypto_alg_mod_lookup(const char *name,
u32 type, u32 mask)
{
struct crypto_alg *alg;
/* 1. 이미 등록된 알고리즘 검색 */
alg = crypto_alg_lookup(name, type, mask);
if (alg)
return alg;
/* 2. 모듈 자동 로드 시도 (MODULE_ALIAS_CRYPTO 매칭) */
request_module("crypto-%s", name);
/* 3. larval (대기 객체) 생성 — 비동기 등록 대기 */
struct crypto_larval *larval = crypto_larval_alloc(name, type, mask);
/* 4. cryptomgr에 알고리즘 탐색 요청 (kthread) */
crypto_probing_notify(CRYPTO_MSG_ALG_REQUEST, larval);
/* 5. 등록 완료 대기 (타임아웃 60초) */
alg = crypto_larval_wait(larval);
/* cryptomgr는 템플릿 매칭을 시도:
* "cbc(aes)" → cbc 템플릿 + aes 알고리즘으로 분해
* → cbc 템플릿의 create() 호출
* → cbc(aes) 인스턴스 자동 생성 및 등록
*/
return alg;
}
FIPS 모드와 자가 테스트
FIPS(Federal Information Processing Standards)는 미국 NIST(National Institute of Standards and Technology)가 발행하는 연방 정보 처리 표준입니다. 암호화 모듈의 보안 요구사항을 정의하며, 미국 연방 정부 기관은 물론 금융, 의료, 방위산업 등 규제 환경에서 필수적으로 요구됩니다. 리눅스 커널은 부트 파라미터를 통해 FIPS 모드를 활성화하여 인증된 암호 알고리즘만 사용하도록 강제할 수 있습니다.
FIPS 140 표준 개요
FIPS 140은 암호화 모듈의 보안 요구사항을 정의하는 NIST 표준으로, 현재 FIPS 140-3(ISO/IEC 19790:2012 기반)이 최신 버전입니다. FIPS 140-2는 2021년 9월 이후 신규 인증이 중단되었으나, 기존 인증은 유효합니다.
| 보안 레벨 | 요구사항 | 적용 예 |
|---|---|---|
| Level 1 | 승인된 알고리즘/함수 사용, 소프트웨어 전용 구현 허용 | 리눅스 커널 Crypto Framework (Crypto API) (소프트웨어) |
| Level 2 | Level 1 + 물리적 탬퍼 증거(tamper-evidence), 역할 기반 인증 | HSM(Hardware Security Module) 기본 등급 |
| Level 3 | Level 2 + 물리적 탬퍼 저항(tamper-resistance), ID 기반 인증 | 결제 단말기, 군사용 장비 |
| Level 4 | Level 3 + 환경적 공격 방어 (전압, 온도 변조 감지) | 최고 보안 등급 하드웨어 모듈 |
리눅스 커널의 FIPS 적용: 커널 Crypto Framework (Crypto API)는 FIPS 요구사항(자가 테스트, 승인 알고리즘 정책, 무결성 검증 연계)의 기술적 기반을 제공합니다. 배포판별 인증 보유 여부와 지원 범위는 릴리스/벤더 정책에 따라 수시로 달라지므로, 최신 상태는 참고자료의 FIPS 지원 상태 섹션에서 확인하세요.
주요 FIPS/NIST 암호화 표준
| 표준 번호 | 명칭 | 내용 | 커널 대응 |
|---|---|---|---|
| FIPS 140-3 | Security Requirements for Cryptographic Modules | 암호 모듈 보안 요구사항 (4단계) | fips_enabled 전역 변수, 자가 테스트 |
| FIPS 197 | Advanced Encryption Standard (AES) | 128/192/256비트 대칭 블록 암호 | aes_generic, aesni_intel |
| FIPS 180-4 | Secure Hash Standard (SHS) | SHA-1, SHA-224/256/384/512 | sha256_generic, sha512_generic |
| FIPS 198-1 | HMAC | 해시 기반 메시지 인증 코드 | hmac 템플릿 |
| FIPS 186-5 | Digital Signature Standard (DSS) | RSA, ECDSA, EdDSA 전자서명 | rsa_generic, ecdsa_generic |
| FIPS 202 | SHA-3 Standard | SHA3-224/256/384/512, SHAKE128/256 | sha3_generic |
| SP 800-38A | Block Cipher Modes | ECB, CBC, CFB, OFB, CTR | ecb, cbc, ctr 템플릿 |
| SP 800-38D | GCM Mode | Galois/Counter Mode (인증 암호화) | gcm 템플릿, gcm-aes-aesni |
| SP 800-38F | Key Wrap | AES Key Wrap (KW, KWP) | kw(aes) |
| SP 800-56A/B | Key Establishment | DH, ECDH 키 합의 | dh_generic, ecdh_generic |
| SP 800-90A | DRBG | 결정론적 난수 생성기 (CTR, Hash, HMAC) | drbg_pr_*, drbg_nopr_* |
| SP 800-132 | PBKDF | 비밀번호 기반 키 유도 | 사용자 공간에서 주로 사용 |
FIPS 모드 활성화
FIPS 140-3 인증이 필요한 환경에서는 커널을 FIPS 모드로 부팅합니다. 이 모드에서는 모든 암호 알고리즘이 반드시 자가 테스트(KAT: Known Answer Test)를 통과해야 사용 가능합니다:
# ─── FIPS 모드 활성화 (부트 파라미터) ───
# GRUB 설정: /etc/default/grub
GRUB_CMDLINE_LINUX="fips=1"
# /boot 파티션이 별도인 경우 boot= 지정 필요
GRUB_CMDLINE_LINUX="fips=1 boot=/dev/sda1"
# GRUB 설정 반영
grub2-mkconfig -o /boot/grub2/grub.cfg
# ─── 현재 FIPS 모드 확인 ───
cat /proc/sys/crypto/fips_enabled
# 1 = FIPS 모드, 0 = 일반 모드
# sysctl로도 확인 가능
sysctl crypto.fips_enabled
# crypto.fips_enabled = 1
# FIPS 모드에서 알고리즘 등록 실패 시 로그
dmesg | grep -i fips
# alg: fips already enabled
# alg: self-tests for myhash256 (myhash256-generic) failed
# (result -2) → 테스트 벡터 불일치
FIPS 모드 전환 시 주의: FIPS 모드는 런타임에 동적으로 전환할 수 없습니다. 반드시 부트 파라미터 fips=1로 커널 초기화 시점에 활성화해야 합니다. 이는 부팅 초기 단계에서 암호 모듈 무결성 검증과 자가 테스트가 수행되어야 하기 때문입니다. /proc/sys/crypto/fips_enabled는 읽기 전용(Read-Only)이며 쓰기가 불가합니다.
FIPS 모드 커널 내부 구현
커널 내부에서 FIPS 모드는 전역 변수 fips_enabled로 관리됩니다. 이 변수는 부팅 시 설정되며, 암호 서브시스템 전반에서 참조합니다:
/* crypto/fips.c — FIPS 모드 전역 상태 */
#include <linux/kernel.h>
#include <linux/sysctl.h>
#include <linux/export.h>
int fips_enabled;
EXPORT_SYMBOL_GPL(fips_enabled);
/* 부트 파라미터 "fips=1" 처리 */
static int __init fips_enable(char *str)
{
fips_enabled = 1;
pr_info("fips mode is enabled\\n");
return 1;
}
__setup("fips=", fips_enable);
/* /proc/sys/crypto/fips_enabled sysctl 등록 */
static struct ctl_table crypto_sysctl_table[] = {
{
.procname = "fips_enabled",
.data = &fips_enabled,
.maxlen = sizeof(int),
.mode = 0444, /* 읽기 전용 */
.proc_handler = proc_dointvec,
},
};
커널 코드 내에서 FIPS 모드를 확인하는 주요 인터페이스:
/* include/linux/fips.h */
#ifdef CONFIG_CRYPTO_FIPS
extern int fips_enabled;
#else
#define fips_enabled 0
#endif
/* 사용 예: 비승인 알고리즘 차단 */
if (fips_enabled && !(alg->cra_flags & CRYPTO_ALG_FIPS_INTERNAL)) {
/* FIPS 비승인 알고리즘 → 등록 거부 */
return -ENOENT;
}
/* CONFIG_CRYPTO_FIPS 커널 설정 옵션 */
/*
* Kconfig (crypto/Kconfig):
* config CRYPTO_FIPS
* bool "FIPS 200 compliance"
* depends on CRYPTO
* help
* This option enables the FIPS boot option which is
* required if you want the system to operate in a
* FIPS 200 compliant manner.
*/
자가 테스트(Self-Test) 메커니즘
FIPS 140-3은 암호 모듈이 전원 투입 자가 테스트(Power-On Self-Test, POST)와 조건부 자가 테스트(Conditional Self-Test, CST)를 수행할 것을 요구합니다. 리눅스 커널에서는 crypto/testmgr.c가 이 역할을 담당합니다:
/* crypto/testmgr.c — 알고리즘 자가 테스트 핵심 구조 */
/* KAT(Known Answer Test) 테스트 벡터 구조체 */
struct cipher_testvec {
const char *key; /* 입력 키 */
const char *iv; /* 초기화 벡터 */
const char *ptext; /* 평문 (Known Input) */
const char *ctext; /* 암호문 (Known Answer) */
unsigned int klen; /* 키 길이 */
unsigned int len; /* 데이터 길이 */
bool fips_skip; /* FIPS 모드에서 건너뛸지 여부 */
};
/* 해시 테스트 벡터 */
struct hash_testvec {
const char *key; /* HMAC 키 (해시는 NULL) */
const char *plaintext; /* 입력 데이터 */
const char *digest; /* 기대 출력 (Known Answer) */
unsigned int psize; /* 평문 길이 */
unsigned int ksize; /* 키 길이 */
};
/* 알고리즘별 테스트 정의 — testmgr.c의 핵심 테이블 */
static const struct alg_test_desc alg_test_descs[] = {
{
.alg = "cbc(aes)",
.test = alg_test_skcipher,
.fips_allowed = 1, /* FIPS 승인 알고리즘 */
.suite = {
.cipher = __VECS(aes_cbc_tv_template)
}
}, {
.alg = "ecb(aes)",
.test = alg_test_skcipher,
.fips_allowed = 1,
.suite = {
.cipher = __VECS(aes_tv_template)
}
}, {
.alg = "sha256",
.test = alg_test_hash,
.fips_allowed = 1,
.suite = {
.hash = __VECS(sha256_tv_template)
}
}, {
.alg = "hmac(sha256)",
.test = alg_test_hash,
.fips_allowed = 1,
.suite = {
.hash = __VECS(hmac_sha256_tv_template)
}
},
/* ... 수백 개의 알고리즘 테스트 정의 ... */
};
테스트 실행 흐름 — 알고리즘이 등록될 때 자동으로 호출됩니다:
/* crypto/testmgr.c — 자가 테스트 실행 흐름 */
/* 1. 알고리즘 등록 시 testmgr가 호출됨 */
int alg_test(const char *driver, const char *alg,
u32 type, u32 mask)
{
const struct alg_test_desc *test;
int rc;
/* alg_test_descs[] 테이블에서 알고리즘 검색 */
test = find_test(alg);
if (!test) {
if (fips_enabled) {
/* FIPS 모드: 테스트 없으면 → 등록 거부 */
pr_err("alg: no test for %s (%s)\\n", alg, driver);
return -EINVAL;
}
/* 일반 모드: 테스트 없으면 → 건너뜀 (경고만) */
return 0;
}
/* 2. FIPS 모드에서 비승인 알고리즘 차단 */
if (fips_enabled && !test->fips_allowed) {
pr_info("alg: %s not allowed in fips mode\\n", alg);
return -EINVAL;
}
/* 3. KAT(Known Answer Test) 실행 */
rc = test->test(test, driver, type, mask);
if (rc) {
if (fips_enabled)
panic("alg: self-tests for %s (%s) failed (rc=%d)\\n",
alg, driver, rc);
else
pr_warn("alg: self-tests for %s (%s) failed (rc=%d)\\n",
alg, driver, rc);
}
return rc;
}
코드 설명
crypto/testmgr.c의 alg_test()는 알고리즘 등록 시 자동 호출되어 KAT(Known Answer Test)를 수행하는 자가 테스트 진입점입니다.
- find_test(alg)
alg_test_descs[]테이블에서 알고리즘 이름으로 이진 탐색합니다. 테이블이 알파벳순 정렬이므로 O(log n) 검색이 가능합니다. - FIPS 모드 + 테스트 없음FIPS 모드에서 테스트 벡터가 없는 알고리즘은
-EINVAL로 등록이 거부됩니다. 일반 모드에서는 경고만 출력하고 등록을 허용합니다. - fips_allowed 검사FIPS 모드에서
fips_allowed가 0인 비승인 알고리즘(예: DES, MD5)은 사용이 차단됩니다. - test->test() 호출알고리즘 유형별 테스트 함수(
alg_test_skcipher,alg_test_hash,alg_test_aead등)를 호출하여 KAT를 실행합니다. - 테스트 실패 시 panic()FIPS 모드에서 자가 테스트 실패는
panic()으로 시스템을 즉시 중단합니다. 일반 모드에서는 경고 로그만 기록합니다.
FIPS 모드 동작 주의: FIPS 모드에서 자가 테스트 실패 시 동작은 커널 버전/패치(Patch)셋/배포판 정책에 따라 다를 수 있습니다. 일반적으로 해당 알고리즘 비활성화 또는 시스템 중단(panic()) 정책 중 하나가 적용되므로, 운영 환경에서는 반드시 배포판 보안 가이드를 기준으로 확인해야 합니다.
FIPS 자가 테스트 유형
FIPS 140-3이 요구하는 자가 테스트는 크게 두 가지로 분류됩니다:
| 테스트 유형 | 실행 시점 | 목적 | 커널 구현 |
|---|---|---|---|
| POST (Power-On Self-Test) | 부팅 시 / 모듈 로드 시 | 암호 알고리즘의 올바른 동작 확인 | testmgr.c의 KAT 벡터 테스트 |
| 무결성 테스트 | 부팅 시 / 모듈 로드 시 | 암호 모듈 바이너리 변조 감지 | HMAC-SHA256 기반 모듈 무결성 검증 |
| CST (Conditional Self-Test) | 특정 조건 발생 시 | 키 쌍 일관성, 난수 연속성 등 | DRBG 연속 출력 비교, RSA 키 쌍 검증 |
| KAT (Known Answer Test) | 알고리즘 등록 시 | 알려진 입력→출력 쌍으로 정확성 검증 | testmgr.h의 테스트 벡터 |
| PCT (Pairwise Consistency Test) | 비대칭 키 생성 시 | 공개키-개인키 쌍의 일관성 | RSA/ECDSA 키 생성 후 서명-검증 |
/* KAT 실행 예: 대칭 암호(skcipher) 테스트 */
static int test_skcipher_vec(const char *driver,
const struct cipher_testvec *vec,
struct skcipher_request *req)
{
/* 1. 키 설정 */
crypto_skcipher_setkey(tfm, vec->key, vec->klen);
/* 2. 알려진 평문으로 암호화 수행 */
sg_init_one(&sg, buf, vec->len);
memcpy(buf, vec->ptext, vec->len);
skcipher_request_set_crypt(req, &sg, &sg, vec->len, iv);
err = crypto_skcipher_encrypt(req);
/* 3. 결과를 Known Answer와 비교 */
if (memcmp(buf, vec->ctext, vec->len) != 0) {
pr_err("encryption test failed for %s\\n", driver);
return -EINVAL; /* KAT 실패 */
}
/* 4. 역방향(복호화) 테스트 */
memcpy(buf, vec->ctext, vec->len);
err = crypto_skcipher_decrypt(req);
if (memcmp(buf, vec->ptext, vec->len) != 0) {
pr_err("decryption test failed for %s\\n", driver);
return -EINVAL; /* KAT 실패 */
}
return 0; /* 테스트 통과 */
}
/* 조건부 자가 테스트 예: DRBG 연속 출력 비교 */
static int drbg_healthcheck(struct drbg_state *drbg,
unsigned char *buf)
{
/* FIPS 140-3: 난수 생성기 연속 출력 비교 테스트 (CRNGT)
* 연속된 두 출력 블록이 동일하면 → 난수 생성기 고장 */
if (memcmp(buf, drbg->prev_output, drbg->len) == 0) {
pr_err("drbg: continuous test failed\\n");
if (fips_enabled)
panic("drbg: continuous random number test failed\\n");
return -EFAULT;
}
memcpy(drbg->prev_output, buf, drbg->len);
return 0;
}
코드 설명
crypto/testmgr.c의 KAT(Known Answer Test) 실행 코드로, 대칭 암호의 정확성 검증과 DRBG 난수 생성기의 건전성 검사를 보여줍니다.
- test_skcipher_vec()테스트 벡터(
cipher_testvec)의 알려진 키·평문으로 암호화를 수행한 뒤, 결과를 기대 암호문(vec->ctext)과 바이트 단위로 비교합니다. - sg_init_one() + in-place 처리단일 scatterlist 엔트리로 버퍼를 설정하고, src와 dst를 같은 sg로 지정하여 in-place 암·복호화를 수행합니다.
- 양방향 검증암호화(평문→암호문)와 복호화(암호문→평문) 양쪽 모두 테스트하여 알고리즘의 왕복(round-trip) 정확성을 확인합니다.
- drbg_healthcheck()FIPS 140-3 CRNGT(Continuous Random Number Generator Test)를 구현합니다. 난수 생성기의 연속 출력 두 블록이 동일하면 고장으로 판단하여 FIPS 모드에서는
panic()을 호출합니다.
FIPS 승인/비승인 알고리즘
FIPS 모드에서는 NIST가 승인한 알고리즘만 사용할 수 있습니다. testmgr.c의 fips_allowed 플래그로 구분합니다:
| 분류 | FIPS 승인 (사용 가능) | FIPS 비승인 (차단) |
|---|---|---|
| 블록 암호 | AES-128/192/256, 3DES (제한적) | Blowfish, Twofish, Serpent, Camellia, CAST5/6, DES |
| 운용 모드 | ECB, CBC, CTR, GCM, CCM, XTS, KW | — |
| 해시 | SHA-1 (서명 검증만), SHA-224/256/384/512, SHA3-* | MD4, MD5, RIPEMD-160 |
| MAC | HMAC-SHA-*, CMAC-AES, GMAC | HMAC-MD5 |
| 비대칭 | RSA (≥2048bit), ECDSA (P-256/384/521), EdDSA | RSA <2048bit |
| 키 합의 | DH (≥2048bit), ECDH (P-256/384/521) | DH <2048bit |
| 난수 생성 | CTR_DRBG, Hash_DRBG, HMAC_DRBG | ANSI X9.31 PRNG (폐기됨) |
| 키 유도 | SP 800-108 KDF (HMAC/CMAC 기반) | — |
3DES 주의: NIST SP 800-131A Rev.2에 따라 3DES(Triple DES)는 2023년 이후 암호화에 사용이 금지(disallowed)되었으며, 복호화에만 제한적으로 허용됩니다. 새로운 구현에서는 반드시 AES를 사용하세요.
CRYPTO_ALG_FIPS_INTERNAL 플래그
커널은 CRYPTO_ALG_FIPS_INTERNAL 플래그로 FIPS 인증 경계(Boundary) 내의 알고리즘과 내부 도우미 알고리즘을 구분합니다:
/* include/linux/crypto.h */
/* FIPS 내부 전용 알고리즘 표시 — 정확한 비트값은 커널 헤더 기준 */
#define CRYPTO_ALG_FIPS_INTERNAL /* include/linux/crypto.h 참조 */
/*
* 이 플래그가 설정된 알고리즘:
* - FIPS 모듈 경계 내부에서만 사용 가능
* - AF_ALG 소켓을 통한 사용자 공간 접근 차단
* - /proc/crypto에서 "internal : yes"로 표시
*
* 예: CTR 모드 DRBG의 내부 AES 인스턴스
* → 직접 사용자 접근은 차단, DRBG 내부에서만 사용
*/
/* AF_ALG에서 FIPS_INTERNAL 알고리즘 차단 */
static int alg_accept(struct sock *sk)
{
struct crypto_alg *alg = ...;
/* FIPS 모드: internal 플래그 알고리즘은 AF_ALG로 사용 불가 */
if (alg->cra_flags & CRYPTO_ALG_FIPS_INTERNAL) {
if (fips_enabled)
return -ENOENT;
}
...
}
/* 템플릿에서 FIPS_INTERNAL 전파 예 */
static int cbc_create(struct crypto_template *tmpl,
struct rtattr **tb)
{
/* 내부 알고리즘(예: ecb(aes))은 FIPS_INTERNAL로 마킹 */
inst->alg.base.cra_flags |= (alg->cra_flags & CRYPTO_ALG_FIPS_INTERNAL);
...
}
HMAC 무결성 검증
FIPS 140-3은 암호화 모듈의 바이너리 무결성을 부팅 시 검증할 것을 요구합니다. 리눅스에서는 HMAC-SHA256 기반의 무결성 검증을 수행합니다:
# FIPS 무결성 파일 확인 (RHEL/CentOS 예시)
ls -la /boot/.vmlinuz-$(uname -r).hmac
# -r--------. 1 root root 33 Jan 15 12:00 /boot/.vmlinuz-6.x.hmac
# 커널 모듈 HMAC 파일
ls /lib/modules/$(uname -r)/.*.hmac
# .aesni-intel.ko.hmac
# .sha256_ssse3.ko.hmac
# .ghash-clmulni-intel.ko.hmac
# 수동 무결성 검증
sha256hmac /boot/vmlinuz-$(uname -r)
# 출력된 HMAC 값이 .vmlinuz-*.hmac 파일 내용과 일치해야 함
# FIPS 모듈 목록 확인 (dracut 기준)
cat /etc/dracut.conf.d/40-fips.conf
# add_dracutmodules+=" fips "
일반 모드 vs FIPS 모드 상세 비교
| 구분 | 일반 모드 | FIPS 모드 |
|---|---|---|
| 자가 테스트 실패 | 경고 로그 또는 알고리즘 비활성화 (구현별 상이) | 알고리즘 비활성화 또는 시스템 중단(정책 의존) |
| 테스트 벡터 없음 | 알고리즘별로 처리 방식 상이 | 정책에 따라 등록 거부 가능 |
| 비승인 알고리즘 | 사용 가능 | 등록 거부 또는 CRYPTO_ALG_FIPS_INTERNAL로 제한 |
| 무결성 검증 | 없음 | 커널/모듈 HMAC-SHA256 무결성 검증 |
| 난수 생성기 | 모든 RNG 사용 가능 | DRBG(SP 800-90A)만 허용, CRNGT 필수 |
| 키 길이 제한 | 없음 | RSA ≥2048bit, DH ≥2048bit 등 최소 길이 강제 |
| AF_ALG 접근 | 모든 알고리즘 | FIPS 승인 + 비-internal 알고리즘만 |
| 알고리즘 우선순위 | priority 값 기준 | FIPS 승인 알고리즘 우선 (비승인 fallback 차단) |
/proc/crypto | 모든 알고리즘 표시 | FIPS 승인 알고리즘만 외부 노출 |
| 커널 설정 | CONFIG_CRYPTO | CONFIG_CRYPTO + CONFIG_CRYPTO_FIPS |
FIPS 모드가 영향을 미치는 커널 서브시스템
FIPS 모드 활성화는 Crypto Framework (Crypto API)뿐 아니라 암호화를 사용하는 모든 커널 서브시스템에 영향을 미칩니다:
| 서브시스템 | FIPS 모드 영향 | 구체적 변경 |
|---|---|---|
| dm-crypt | FIPS 승인 암호만 허용 | aes-xts-plain64 사용, serpent/twofish 차단 |
| IPsec (XFRM) | FIPS 승인 알고리즘만 SA 설정 가능 | AES-GCM, AES-CBC + HMAC-SHA256 허용, Blowfish/CAST 차단 |
| IKE (strongSwan/Libreswan) | FIPS 정책 연동 | DH group 14(2048bit) 이상만 허용 |
| TLS (kTLS) | FIPS 승인 cipher suite만 | AES-128/256-GCM, AES-256-CCM 허용 |
| 네트워크 (TCP-MD5) | MD5 기반 인증 차단 | FIPS 모드에서 TCP-MD5 옵션 비활성화 |
| 디스크 암호화 (LUKS) | FIPS 호환 파라미터 강제 | PBKDF2-SHA256 + AES-256-XTS, argon2id 차단 |
| 난수 (/dev/random) | DRBG 기반으로 전환 | SP 800-90A DRBG, CRNGT(연속 출력 비교) 활성화 |
| 모듈 서명 | SHA-256/384/512만 허용 | CONFIG_MODULE_SIG_HASH에 SHA-1 사용 불가 |
| 커널 키링(Keyring) | FIPS 승인 알고리즘으로 키 연산 | RSA ≥2048bit, ECDSA P-256+ 인증서만 허용 |
배포판/인증 상태 분리: 배포판별 FIPS 인증 보유 여부, 활성화 절차, CAVP/CMVP 진행 상태는 시간이 지나면 빠르게 바뀝니다. 이 페이지에서는 커널 내부 동작에 집중하고, 운영 상태 정보는 참고자료 - FIPS 지원/인증 상태로 위임합니다.
FIPS 모드 디버깅(Debugging)
# ─── FIPS 모드 상태 종합 확인 ───
# 1. FIPS 모드 활성화 여부
cat /proc/sys/crypto/fips_enabled
# 2. 부팅 시 FIPS 관련 커널 로그
dmesg | grep -i fips
# [ 0.000000] command line: ... fips=1
# [ 0.123456] fips mode is enabled
# 3. 자가 테스트 결과 확인
dmesg | grep "alg: self-tests"
# alg: self-tests for sha256 (sha256-generic) passed
# alg: self-tests for aes (aes-generic) passed
# alg: self-tests for cbc(aes) (cbc-aes-aesni) passed
# 4. FIPS 승인 알고리즘 목록 확인
grep -A1 "selftest" /proc/crypto | grep -B1 "passed"
# 5. FIPS 모드에서 차단된 알고리즘 확인
dmesg | grep "not allowed in fips"
# alg: md5 not allowed in fips mode
# 6. 무결성 검증 실패 확인
dmesg | grep -i "integrity"
# integrity: HMAC check failed for vmlinuz
# 7. 커널 FIPS 설정 옵션 확인
zgrep CONFIG_CRYPTO_FIPS /proc/config.gz
# CONFIG_CRYPTO_FIPS=y
# 8. 현재 사용 중인 알고리즘의 FIPS 상태
# /proc/crypto에서 fips 관련 필드 확인
awk '/^name/{name=$NF} /^selftest/{print name, $NF}' /proc/crypto
# 9. FIPS 모드에서 OpenSSL 상태 확인 (사용자 공간 연동)
openssl version
openssl list -providers
# fips 프로바이더가 활성화되어 있어야 함
FIPS 모드 트러블슈팅: (1) 부팅 실패 시 fips=1 제거 후 재부팅하여 원인 파악, (2) 모듈 로드 실패 시 .hmac 파일 존재 여부와 커널 버전 일치 확인, (3) 애플리케이션 오류 시 crypto-policies 설정과 라이브러리 FIPS 모드 연동 확인, (4) VM 환경에서는 호스트의 AES-NI/SHA-NI CPU 플래그 패스스루 확인이 필요합니다.
/proc/crypto 상세 해석
/proc/crypto는 현재 등록된 모든 알고리즘의 상세 정보를 보여줍니다:
# /proc/crypto 출력 예시 — AES-NI CBC 구현
name : cbc(aes)
driver : cbc-aes-aesni
module : aesni_intel
priority : 400
refcnt : 9
selftest : passed
internal : no
type : skcipher
async : no
blocksize : 16
min keysize : 16
max keysize : 32
ivsize : 16
chunksize : 16
walksize : 16
| 필드 | 의미 | 확인 포인트 |
|---|---|---|
name | 정규 알고리즘 이름 | crypto_alloc_*()에서 사용하는 이름 |
driver | 드라이버 고유 이름 | 어떤 구현체인지 식별 (generic, aesni, qat 등) |
module | 소속 커널 모듈 | lsmod와 대조하여 모듈 의존성 확인 |
priority | 우선순위 | 같은 name 중 가장 높은 값이 실제 사용됨 |
refcnt | 현재 참조 횟수(Reference Count) | 0이 아니면 사용 중 (모듈 언로드 불가) |
selftest | 자가 테스트 결과 | passed / unknown / not available |
internal | 내부 전용 여부 | yes면 AF_ALG에서 사용 불가 |
async | 비동기 여부 | H/W 가속기 구현은 보통 yes |
# 실용적인 /proc/crypto 분석 명령
# 같은 알고리즘의 모든 구현체와 priority 비교
awk '/^name/{n=$3} /^driver/{d=$3} /^priority/{print n, d, $3}' /proc/crypto \
| sort | column -t
# 특정 알고리즘의 실제 사용되는 구현 확인 (priority 최고)
awk '/^name/{n=$3} /^driver/{d=$3} /^priority/{p=$3}
n=="cbc(aes)"{print p, d}' /proc/crypto | sort -rn | head -1
# 자가 테스트 실패한 알고리즘 찾기
awk '/^name/{n=$3} /^selftest/{if($3!="passed") print n, $3}' /proc/crypto
# 비동기(H/W) 알고리즘만 확인
awk '/^name/{n=$3} /^async/{if($3=="yes") print n}' /proc/crypto
SIMD 컨텍스트 관리
SSE/AVX/NEON 명령어를 사용하는 H/W 가속 구현에서는 SIMD 레지스터 컨텍스트 관리가 필수입니다. 커널에서 SIMD 레지스터를 사용하려면 명시적으로 저장/복원해야 합니다:
#include <asm/fpu/api.h> /* x86 */
#include <crypto/internal/simd.h>
/* 방법 1: 직접 kernel_fpu_begin/end 사용 */
static int my_aesni_encrypt(struct skcipher_request *req)
{
/* kernel_fpu_begin()은 현재 태스크의 FPU 상태를 저장하고
* 커널이 SSE/AVX 레지스터를 사용할 수 있게 함
* 주의: 이 구간에서는 preemption이 비활성화됨 */
kernel_fpu_begin();
/* AES-NI 명령어 사용 (인라인 어셈블리 또는 .S 파일) */
aesni_cbc_enc(ctx->key_sched, dst, src, len, iv);
kernel_fpu_end();
return 0;
}
/* 방법 2: crypto_simd 래퍼 사용 (권장) */
/* softirq/hardirq 컨텍스트에서도 안전하게 동작
* SIMD를 사용할 수 없는 컨텍스트에서는 자동으로
* cryptd kthread로 작업을 위임 */
static struct simd_skcipher_alg *simd_alg;
static int __init my_simd_init(void)
{
int err;
/* 원본 SIMD 알고리즘 등록 (__cbc-aes-aesni, INTERNAL 플래그) */
err = crypto_register_skcipher(&my_simd_skcipher);
if (err)
return err;
/* SIMD 래퍼 등록 (cbc-aes-aesni, 외부 공개)
* → process context: 직접 SIMD 실행
* → softirq context: cryptd kthread로 위임 */
simd_alg = simd_skcipher_create_compat(
my_simd_skcipher.base.cra_name,
my_simd_skcipher.base.cra_driver_name,
my_simd_skcipher.base.cra_name);
return PTR_ERR_OR_ZERO(simd_alg);
}
kernel_fpu_begin()과kernel_fpu_end()사이에서는 sleep 불가 (preemption 비활성화)- softirq/hardirq 컨텍스트에서는
kernel_fpu_begin()호출 불가 —crypto_simd래퍼를 사용하거나,irq_fpu_usable()로 확인 후 fallback - 대량 데이터 처리 시 주기적으로
kernel_fpu_end(); kernel_fpu_begin();으로 preemption 허용 (latency 방지) - ARM의 경우
kernel_neon_begin()/kernel_neon_end()사용
알고리즘 구현 체크리스트
| # | 단계 | 파일 | 핵심 확인사항 |
|---|---|---|---|
| 1 | 알고리즘 구현 | crypto/my_alg.c | 콜백 함수 모두 구현, 에러 처리, 메모리 해제 |
| 2 | 헤더 포함 | crypto/my_alg.c | <crypto/internal/hash.h> 등 올바른 헤더 |
| 3 | 알고리즘 등록 | crypto/my_alg.c | crypto_register_*() + module_init/exit |
| 4 | MODULE_ALIAS | crypto/my_alg.c | MODULE_ALIAS_CRYPTO("name") 추가 |
| 5 | 테스트 벡터 | crypto/testmgr.h | 최소 3개 이상의 KAT 벡터 (NIST 표준 권장) |
| 6 | 테스트 등록 | crypto/testmgr.c | alg_test_descs[]에 알파벳순 추가 |
| 7 | Kconfig | crypto/Kconfig | config CRYPTO_MY_ALG + select 의존성 |
| 8 | Makefile | crypto/Makefile | obj-$(CONFIG_CRYPTO_MY_ALG) 추가 |
| 9 | 셀프 테스트 | 부팅/로드 시 | dmesg | grep selftest로 passed 확인 |
| 10 | 벤치마크 | 런타임 | tcrypt 모듈로 성능 측정 |
커널 소스 참고: 새 알고리즘을 구현할 때는 반드시 커널 트리의 유사 구현을 참고하세요. 해시는 crypto/sha256_generic.c, 블록 암호는 crypto/aes_generic.c, CBC 모드는 crypto/cbc.c, AEAD는 crypto/gcm.c, H/W 가속은 arch/x86/crypto/aesni-intel_glue.c가 좋은 참고 자료입니다.
crypto_alg 등록 흐름
알고리즘 등록은 Crypto Framework (Crypto API)의 핵심 진입점(Entry Point)입니다. crypto_register_alg() 호출 시 커널 내부에서는 이름 충돌 검사, 우선순위 비교, 테스트 벡터 검증, larval 깨우기(Wakeup), 템플릿 인스턴스화가 순차적으로 수행됩니다. 이 섹션에서는 등록 과정의 내부 흐름을 단계별로 추적합니다.
crypto_register_alg() 내부 흐름
crypto_register_alg()는 crypto/algapi.c에 정의되어 있으며, 호출 시 다음 순서로 동작합니다:
/* crypto/algapi.c — crypto_register_alg() 핵심 로직 (간략화) */
int crypto_register_alg(struct crypto_alg *alg)
{
struct crypto_alg *q;
int ret = 0;
/* 1. 입력 유효성 검사 */
if (!alg->cra_name[0] || !alg->cra_driver_name[0])
return -EINVAL;
/* blocksize가 PAGE_SIZE를 초과하면 거부 */
if (alg->cra_blocksize > PAGE_SIZE)
return -EINVAL;
/* refcnt 초기화 */
refcount_set(&alg->cra_refcnt, 1);
/* 2. 전역 알고리즘 리스트 잠금 (write lock) */
down_write(&crypto_alg_sem);
/* 3. 같은 driver_name이 이미 등록되어 있는지 확인 */
list_for_each_entry(q, &crypto_alg_list, cra_list) {
if (!strcmp(q->cra_driver_name, alg->cra_driver_name)) {
ret = -EEXIST;
goto out_unlock;
}
}
/* 4. 알고리즘 리스트에 삽입 */
list_add(&alg->cra_list, &crypto_alg_list);
/* 5. 대기 중인 larval 객체 깨우기
* crypto_alloc_*()에서 대기 중이던 요청에 통보 */
crypto_alg_finish_registration(alg);
up_write(&crypto_alg_sem);
/* 6. 알림 체인: testmgr, cryptomgr에 등록 이벤트 전달
* → testmgr: 자가 테스트 스케줄링
* → cryptomgr: 템플릿 인스턴스화 트리거 */
crypto_probing_notify(CRYPTO_MSG_ALG_REGISTER, alg);
return 0;
out_unlock:
up_write(&crypto_alg_sem);
return ret;
}
코드 설명
crypto/algapi.c의 crypto_register_alg()는 새 암호 알고리즘을 커널 전역 리스트에 등록하는 핵심 함수입니다.
- 입력 유효성 검사
cra_name과cra_driver_name이 비어 있으면-EINVAL을 반환합니다.cra_blocksize가PAGE_SIZE를 초과하는 경우도 거부하여 scatterlist 처리의 안전성을 보장합니다. - down_write(&crypto_alg_sem)전역 알고리즘 리스트를 보호하는 rw_semaphore의 write lock을 획득합니다. 등록은 배타적이어야 하므로 write lock을 사용합니다.
- driver_name 중복 검사
crypto_alg_list를 순회하며 동일한cra_driver_name이 이미 있으면-EEXIST를 반환합니다. 같은cra_name은 허용되지만 driver_name은 고유해야 합니다. - crypto_alg_finish_registration()이 알고리즘을 기다리던 larval 객체들을 깨웁니다.
crypto_alloc_*()에서 대기 중이던 호출자가 새로 등록된 알고리즘을 사용할 수 있게 됩니다. - crypto_probing_notify()등록 이벤트를 알림 체인으로 전달합니다. testmgr는 자가 테스트를 스케줄링하고, cryptomgr는 등록된 블록 암호에 대해 cbc, ctr, xts 등의 템플릿 인스턴스를 자동 생성합니다.
우선순위 선택과 동적 교체
같은 cra_name에 여러 구현이 등록되면, crypto_alloc_*() 시점에 priority가 가장 높은 구현이 선택됩니다. 나중에 더 높은 priority의 구현이 등록되면 새로운 tfm 할당부터 적용됩니다:
/* crypto/api.c — 알고리즘 선택 로직 (간략화) */
static struct crypto_alg *crypto_find_alg(const char *name,
u32 type, u32 mask)
{
struct crypto_alg *best = NULL;
struct crypto_alg *q;
list_for_each_entry(q, &crypto_alg_list, cra_list) {
/* cra_name이 요청과 일치하는지 확인 */
if (strcmp(q->cra_name, name) != 0)
continue;
/* type/mask 필터 적용 */
if ((q->cra_flags ^ type) & mask)
continue;
/* priority가 더 높은 구현 선택 */
if (!best || q->cra_priority > best->cra_priority)
best = q;
}
return best;
}
/* 우선순위 예시:
* "aes" → aes_generic(100) vs aes-aesni(300) vs qat_aes(4001)
* → QAT 드라이버 로드 시 qat_aes(4001)가 자동 선택
* → QAT 드라이버 unload 시 aes-aesni(300)로 자동 전환
* 기존 tfm은 영향 없음, 새 crypto_alloc_*() 호출부터 적용 */
코드 설명
crypto/api.c의 알고리즘 선택 로직으로, crypto_alloc_*() 시점에 가장 적합한 구현을 탐색합니다.
- crypto_find_alg()전역
crypto_alg_list를 순회하며 요청된cra_name과 일치하는 알고리즘을 찾습니다. 이 탐색은 read lock 하에서 수행됩니다. - type/mask 필터
(q->cra_flags ^ type) & mask연산으로 호출자가 요구하는 속성(비동기, 커널 전용 등)을 충족하지 않는 알고리즘을 필터링합니다. - priority 비교조건을 만족하는 알고리즘 중
cra_priority가 가장 높은 것을 선택합니다. H/W 드라이버 로드·언로드에 따라 동적으로 최적 구현이 변경되며, 이미 할당된 tfm에는 영향을 주지 않습니다.
템플릿 인스턴스화 메커니즘
템플릿은 알고리즘 등록 시 CRYPTO_MSG_ALG_REGISTER 알림을 받아 자동으로 인스턴스를 생성합니다. 이 메커니즘을 통해 새 블록 암호를 추가하면 기존 모든 운용 모드와 자동 조합됩니다:
/* crypto/algboss.c — 템플릿 인스턴스화 트리거 */
static int cryptomgr_notify(struct notifier_block *this,
unsigned long msg, void *data)
{
switch (msg) {
case CRYPTO_MSG_ALG_REQUEST:
/* 알고리즘 요청 시: 템플릿 매칭 → 인스턴스 생성 */
return cryptomgr_schedule_probe(data);
case CRYPTO_MSG_ALG_REGISTER:
/* 새 알고리즘 등록 시: 대기 중인 larval과 매칭 */
return cryptomgr_schedule_test(data);
}
return NOTIFY_DONE;
}
/* 인스턴스 생성 과정 (예: "cbc(sm4)" 요청 시) */
/*
* 1. "cbc(sm4)" 파싱 → 템플릿 이름 "cbc" + 내부 알고리즘 "sm4"
* 2. crypto_template 목록에서 "cbc" 탐색
* 3. cbc_tmpl->create() 호출:
* a. sm4 알고리즘 참조 (spawn) 획득
* b. skcipher_instance 할당
* c. 이름/속성 설정: cra_name = "cbc(sm4)"
* d. 내부 알고리즘 속성 상속 (blocksize, keysize 등)
* e. skcipher_register_instance() 로 등록
* 4. 등록 완료 → 대기 중인 larval 깨우기
* 5. testmgr가 자가 테스트 수행
*/
등록 순서와 모듈 의존성: 템플릿 인스턴스화는 내부 알고리즘이 먼저 등록되어 있어야 합니다. "cbc(aes)"를 요청하면 커널은 먼저 crypto-aes 모듈 로드를 시도한 후 crypto-cbc 템플릿 모듈을 로드합니다. 두 모듈이 모두 로드되면 cbc 템플릿의 create()가 호출되어 인스턴스가 자동 생성됩니다. 이 과정에서 MODULE_ALIAS_CRYPTO 매크로가 핵심 역할을 합니다.
AEAD 상태 머신
AEAD(Authenticated Encryption with Associated Data) 알고리즘은 암호화와 인증을 하나의 원자적 연산(Atomic Operation)으로 결합합니다. 내부적으로 encrypt 경로와 decrypt 경로는 비대칭적이며, 특히 decrypt에서는 인증 태그 검증이 복호화보다 먼저 수행됩니다. 이 섹션에서는 커널의 authenc 구현을 기준으로 AEAD의 상태 전이를 분석합니다.
Encrypt 경로 상태 전이
authenc 내부 구현 분석
커널의 crypto/authenc.c는 가장 기본적인 AEAD 구현으로, Encrypt-then-MAC 방식을 사용합니다. IPsec ESP에서 authenc(hmac(sha256),cbc(aes)) 형태로 주로 사용됩니다:
/* crypto/authenc.c — authenc encrypt 핵심 경로 (간략화) */
static int crypto_authenc_encrypt(struct aead_request *req)
{
struct crypto_aead *authenc = crypto_aead_reqtfm(req);
struct authenc_instance_ctx *ictx = aead_instance_ctx(
aead_alg_instance(authenc));
struct authenc_request_ctx *areq_ctx = aead_request_ctx(req);
struct crypto_skcipher *enc = ictx->enc;
int err;
/* 1단계: skcipher로 평문 암호화 */
struct skcipher_request *subreq = &areq_ctx->subreq;
skcipher_request_set_tfm(subreq, enc);
skcipher_request_set_crypt(subreq, req->src, req->dst,
req->cryptlen, req->iv);
skcipher_request_set_callback(subreq, aead_request_flags(req),
authenc_encrypt_done, req);
err = crypto_skcipher_encrypt(subreq);
if (err)
return err;
/* 2단계: AAD + 암호문에 대해 HMAC 계산 */
return crypto_authenc_genicv(req, aead_request_flags(req));
}
/* authenc decrypt: MAC 검증이 먼저! */
static int crypto_authenc_decrypt(struct aead_request *req)
{
struct crypto_aead *authenc = crypto_aead_reqtfm(req);
unsigned int authsize = crypto_aead_authsize(authenc);
int err;
/* 1단계: AAD + 암호문에 대해 HMAC 재계산 */
err = crypto_authenc_verify(req);
if (err)
return err; /* -EBADMSG: 태그 불일치 → 복호화 거부 */
/* 2단계: 태그 일치 확인 후에만 복호화 수행 */
return crypto_authenc_decrypt_tail(req,
aead_request_flags(req));
}
/* 태그 비교: 반드시 상수 시간(constant-time) 비교 사용 */
static int crypto_authenc_verify(struct aead_request *req)
{
/* ... HMAC 계산 ... */
/* crypto_memneq: 타이밍 부채널 방지 상수 시간 비교
* memcmp와 달리 첫 불일치에서 조기 종료하지 않음 */
if (crypto_memneq(computed_tag, received_tag, authsize))
return -EBADMSG;
return 0;
}
AEAD scatterlist 메모리 레이아웃
AEAD 요청의 scatterlist 레이아웃은 encrypt와 decrypt에서 비대칭적입니다. 이 차이를 정확히 이해하지 못하면 데이터 손상이나 인증 실패가 발생합니다:
| 방향 | req->src 레이아웃 | req->dst 레이아웃 | req->cryptlen 의미 |
|---|---|---|---|
| Encrypt | [AAD | 평문] | [AAD | 암호문 | 태그] | 평문 길이 |
| Decrypt | [AAD | 암호문 | 태그] | [AAD | 평문] | 암호문 + 태그 길이 |
/* AEAD scatterlist 구성 예제 (IPsec ESP 기준) */
struct scatterlist sg[3];
unsigned int authsize = crypto_aead_authsize(aead);
/* === Encrypt 시 === */
/* src: [AAD(assoclen) | 평문(cryptlen)]
* dst: [AAD(assoclen) | 암호문(cryptlen) | 태그(authsize)] */
sg_init_table(sg, 3);
sg_set_buf(&sg[0], aad, assoclen);
sg_set_buf(&sg[1], plaintext, plaintext_len);
sg_set_buf(&sg[2], tag_space, authsize); /* 태그 공간 확보 */
aead_request_set_crypt(req, sg, sg, plaintext_len, iv);
aead_request_set_ad(req, assoclen);
/* === Decrypt 시 === */
/* src: [AAD(assoclen) | 암호문+태그(cryptlen)]
* cryptlen = 암호문 길이 + authsize
* 실제 평문 길이 = cryptlen - authsize */
aead_request_set_crypt(req, sg, sg,
ciphertext_len + authsize, /* 태그 포함! */
iv);
aead_request_set_ad(req, assoclen);
코드 설명
AEAD(Authenticated Encryption with Associated Data) 연산에서 scatterlist를 구성하는 IPsec ESP 기준 예제입니다. Encrypt와 Decrypt의 비대칭 레이아웃이 핵심입니다.
- sg_init_table() / sg_set_buf()scatterlist 배열을 초기화하고 각 엔트리에 AAD, 평문/암호문, 태그 버퍼를 매핑합니다.
include/linux/scatterlist.h에 정의된 API입니다. - Encrypt: sg[2] = tag_space암호화 시 출력에 인증 태그가 추가되므로, 태그 크기(
crypto_aead_authsize())만큼의 추가 공간을 scatterlist에 미리 확보해야 합니다. - aead_request_set_crypt(req, sg, sg, ...)src와 dst를 같은 sg로 지정하면 in-place 처리가 됩니다. Encrypt의
cryptlen은 평문 길이, Decrypt의cryptlen은 암호문+태그 길이로 의미가 다릅니다. - Decrypt: ciphertext_len + authsize복호화 시
cryptlen에 인증 태그 크기를 반드시 포함해야 합니다. 누락하면 태그가 잘려서 인증 검증이 무의미해집니다. - aead_request_set_ad(req, assoclen)AAD(Additional Authenticated Data) 길이를 지정합니다. 이 길이만큼의 scatterlist 앞부분은 인증에만 포함되고 암·복호화 대상에서는 제외됩니다.
- decrypt의 cryptlen에 authsize 미포함 — 태그가 입력에서 잘리고 검증이 무의미해짐
- Decrypt 시 MAC 검증 전에 복호화 수행 — 변조된 암호문의 평문이 노출되는 보안 취약점(Vulnerability)
- memcmp로 태그 비교 — 타이밍 부채널 공격에 취약. 반드시
crypto_memneq()사용 - in-place 처리 시 src/dst 오프셋(Offset) 혼동 — AAD 영역은 변경되지 않지만 offset 계산에 포함
실전 구현 패턴과 다양한 예시
앞 절에서는 Crypto API의 각 계층과 가속 메커니즘을 나누어 보았지만, 실제 커널 코드는 거의 항상 여러 층을 동시에 건드립니다. 같은 AES라도 어떤 경로는 sleep 가능한 process context에서 동작하고, 어떤 경로는 softirq에서 바로 들어오며, 어떤 경로는 DMA 가능한 scatterlist와 함께 hardware queue에 실려 나갑니다. 따라서 실전 구현에서는 알고리즘 이름보다도 문맥 제약, 객체 수명, 입출력 버퍼 레이아웃, fallback 경로, 에러 전파를 먼저 설계해야 합니다.
아래 예시들은 단순히 "이 함수는 이렇게 호출합니다"를 넘어서, 실제 서브시스템 코드가 어떤 틀로 조립되는지 보여 주는 데 초점을 둡니다. 특히 기존 문서의 개별 API 예시를 보완하기 위해, 이번 섹션은 소비자 관점에서 자주 필요한 패턴을 묶어서 설명합니다.
| 패턴 | 핵심 API | 주로 쓰는 위치 | 핵심 포인트 |
|---|---|---|---|
| 동기 대기 래퍼 | DECLARE_CRYPTO_WAIT, crypto_wait_req() | fscrypt, 블록 계층, 제어 경로 | async 구현체도 같은 코드로 수용하되 sleep 가능한 문맥에서만 기다립니다. |
| 다중 버퍼 해시 | crypto_ahash, scatterlist | 네트워크 패킷, 파일 검증, firmware blob | 연속 버퍼로 memcpy하지 않고 조각 난 데이터를 바로 해시합니다. |
| AEAD 레코드 처리 | crypto_aead, aead_request_set_ad() | kTLS, IPsec ESP, 저장 포맷 | AAD, ciphertext, tag의 배치를 정확히 이해해야 합니다. |
| HMAC 기반 키 파생 | crypto_shash, hmac(sha256) | 세션 키 파생, 방향별 키 분리 | E2E 프로토콜은 보통 공유 비밀을 바로 쓰지 않고 다시 KDF에 통과시킵니다. |
| DRBG 직접 사용 | crypto_rng | FIPS 경계, 재현 가능한 테스트 | 일반 난수는 get_random_bytes(), 정책형 DRBG는 crypto_rng가 맞습니다. |
| 압축 오프로드 | crypto_comp, crypto_acomp | zswap, 압축 가속기, storage pipeline | 동기/비동기 압축 API가 별도로 존재합니다. |
| 서명 검증 파이프라인 | crypto_shash + crypto_sig | 모듈, 펌웨어, secure boot chain | 대부분의 서명 API는 메시지 본문이 아니라 digest를 입력으로 받습니다. |
| 하드웨어 큐 드라이버 | crypto_engine | SoC crypto 엔진, PCI 가속기 | 큐잉, IRQ 완료, request 수명 관리를 프레임워크에 위임합니다. |
비동기 구현을 동기처럼 다루는 skcipher 패턴
Crypto API의 많은 구현체는 내부적으로 비동기입니다. 하지만 상위 계층이 sleep 가능한 process context라면 굳이 콜백 기반 상태 머신으로 코드를 찢지 않고, DECLARE_CRYPTO_WAIT와 crypto_wait_req()로 "동기처럼" 다룰 수 있습니다. 이 패턴은 하드웨어 엔진이 있으면 비동기로 돌고, 없으면 generic 구현으로 즉시 끝나는 두 경우를 같은 코드로 수용하는 점이 강점입니다.
#include <crypto/skcipher.h>
#include <linux/crypto.h>
#include <linux/scatterlist.h>
static int encrypt_ctr_two_segments(const u8 *key, unsigned int key_len,
const u8 iv[16],
const u8 *head, unsigned int head_len,
const u8 *body, unsigned int body_len,
u8 *out_head, u8 *out_body)
{
struct crypto_skcipher *tfm;
struct skcipher_request *req;
struct scatterlist src[2], dst[2];
u8 iv_local[16];
unsigned int total = head_len + body_len;
int ret;
DECLARE_CRYPTO_WAIT(wait);
tfm = crypto_alloc_skcipher("ctr(aes)", 0, 0);
if (IS_ERR(tfm))
return PTR_ERR(tfm);
ret = crypto_skcipher_setkey(tfm, key, key_len);
if (ret)
goto out_free_tfm;
req = skcipher_request_alloc(tfm, GFP_KERNEL);
if (!req) {
ret = -ENOMEM;
goto out_free_tfm;
}
sg_init_table(src, 2);
sg_set_buf(&src[0], head, head_len);
sg_set_buf(&src[1], body, body_len);
sg_init_table(dst, 2);
sg_set_buf(&dst[0], out_head, head_len);
sg_set_buf(&dst[1], out_body, body_len);
memcpy(iv_local, iv, 16);
skcipher_request_set_callback(req,
CRYPTO_TFM_REQ_MAY_SLEEP | CRYPTO_TFM_REQ_MAY_BACKLOG,
crypto_req_done, &wait);
skcipher_request_set_crypt(req, src, dst, total, iv_local);
ret = crypto_wait_req(crypto_skcipher_encrypt(req), &wait);
memzero_explicit(iv_local, sizeof(iv_local));
skcipher_request_free(req);
out_free_tfm:
crypto_free_skcipher(tfm);
return ret;
}
| 포인트 | 왜 중요한가 | 실무 메모 |
|---|---|---|
crypto_wait_req() | 즉시 완료와 -EINPROGRESS/-EBUSY를 동일 경로로 수렴합니다. | sleep 가능한 문맥에서만 사용해야 하며, atomic/softirq 경로에서는 기다리면 안 됩니다. |
| IV 복사본 사용 | 운용 모드에 따라 IV를 워크 버퍼처럼 다루는 구현이 있어 원본 재사용이 위험합니다. | 요청마다 iv_local을 두고 완료 후 소거하는 습관이 안전합니다. |
| tfm은 장수 객체 | 키 확장과 fallback 준비는 비쌉니다. | socket/inode/queue 수명에 맞춰 캐시하고, 패킷마다 alloc/free 하지 않는 편이 좋습니다. |
| request는 in-flight 단위 | request 안에는 callback과 reqctx가 묶여 있으므로 동시에 두 작업에 재사용할 수 없습니다. | 동시 요청 수가 8개면 request도 최소 8개가 필요합니다. |
ctr(aes)가 설명하기 좋습니다.
같은 패턴을 xts(aes)에 적용하면 블록 계층, cbc(aes)에 적용하면 레거시 프로토콜 코드와 닮아집니다.
ahash와 멀티 세그먼트 scatterlist
shash는 작은 연속 버퍼에 가장 단순하지만, 실제 커널 데이터는 헤더와 payload, trailer가 제각각 다른 메모리 조각에 놓여 있는 경우가 많습니다. 이때 crypto_ahash는 scatterlist를 직접 받아 복사 없이 해시를 계산할 수 있고, DMA 기반 해시 엔진과도 자연스럽게 연결됩니다.
#include <crypto/hash.h>
#include <linux/crypto.h>
#include <linux/scatterlist.h>
static int calc_sha256_ahash_three_buffers(const u8 *hdr, unsigned int hdr_len,
const u8 *payload, unsigned int payload_len,
const u8 *tail, unsigned int tail_len,
u8 digest[32])
{
struct crypto_ahash *tfm;
struct ahash_request *req;
struct scatterlist sg[3];
unsigned int total = hdr_len + payload_len + tail_len;
int ret;
DECLARE_CRYPTO_WAIT(wait);
tfm = crypto_alloc_ahash("sha256", 0, 0);
if (IS_ERR(tfm))
return PTR_ERR(tfm);
req = ahash_request_alloc(tfm, GFP_KERNEL);
if (!req) {
ret = -ENOMEM;
goto out_free_tfm;
}
sg_init_table(sg, 3);
sg_set_buf(&sg[0], hdr, hdr_len);
sg_set_buf(&sg[1], payload, payload_len);
sg_set_buf(&sg[2], tail, tail_len);
ahash_request_set_callback(req,
CRYPTO_TFM_REQ_MAY_SLEEP | CRYPTO_TFM_REQ_MAY_BACKLOG,
crypto_req_done, &wait);
ahash_request_set_crypt(req, sg, digest, total);
ret = crypto_wait_req(crypto_ahash_digest(req), &wait);
ahash_request_free(req);
out_free_tfm:
crypto_free_ahash(tfm);
return ret;
}
이 예시는 digest 단일 호출만 보여 주지만, 실전에서는 init/update/final 또는 finup 계열로 더 긴 스트림을 처리하기도 합니다. 예를 들어, 대형 펌웨어 이미지를 페이지 단위로 읽어 오면서 해시하려면 crypto_ahash_init() 후 페이지마다 crypto_ahash_update()를 호출하고, 마지막 조각에서 crypto_ahash_final()을 수행하는 식이 더 적합합니다.
| 선택 기준 | shash | ahash |
|---|---|---|
| 입력 버퍼 | 연속 버퍼 중심 | scatterlist 중심 |
| 문맥 | 즉시 계산이 쉬운 제어 경로 | DMA 엔진, 큰 파일, 네트워크 조각 버퍼 |
| 완료 모델 | 동기 | 동기 또는 비동기 |
| 전형적 사례 | 작은 키/메타데이터 HMAC | 패킷 본문, 페이지 캐시(Page Cache), 펌웨어 blob |
AEAD 레코드 처리: AAD, 암호문, 태그 배치
AEAD는 Crypto API에서 가장 자주 길이 계산을 틀리는 인터페이스입니다. 이유는 AAD 길이와 cryptlen이 서로 다른 의미를 갖기 때문입니다. aead_request_set_ad()는 AAD 길이를 별도로 주고, aead_request_set_crypt()의 cryptlen은 "암호화/복호화 대상 본문 길이"를 뜻합니다. 복호화 시에는 인증 태그까지 포함한 길이를 넘겨야 하는 점도 실수 포인트입니다.
#include <crypto/aead.h>
#include <linux/crypto.h>
#include <linux/scatterlist.h>
static int gcm_encrypt_record_inplace(const u8 *key, unsigned int key_len,
const u8 *iv, unsigned int iv_len,
u8 *record, unsigned int aad_len,
unsigned int plaintext_len)
{
struct crypto_aead *tfm;
struct aead_request *req;
struct scatterlist sg;
u8 iv_local[16];
int ret;
DECLARE_CRYPTO_WAIT(wait);
tfm = crypto_alloc_aead("gcm(aes)", 0, 0);
if (IS_ERR(tfm))
return PTR_ERR(tfm);
ret = crypto_aead_setkey(tfm, key, key_len);
if (ret)
goto out_free_tfm;
ret = crypto_aead_setauthsize(tfm, 16);
if (ret)
goto out_free_tfm;
req = aead_request_alloc(tfm, GFP_KERNEL);
if (!req) {
ret = -ENOMEM;
goto out_free_tfm;
}
if (iv_len != crypto_aead_ivsize(tfm)) {
ret = -EINVAL;
goto out_free_req;
}
memcpy(iv_local, iv, iv_len);
/* record 레이아웃: [AAD][plaintext][tag용 여유 16바이트] */
sg_init_one(&sg, record, aad_len + plaintext_len + 16);
aead_request_set_callback(req,
CRYPTO_TFM_REQ_MAY_SLEEP | CRYPTO_TFM_REQ_MAY_BACKLOG,
crypto_req_done, &wait);
aead_request_set_ad(req, aad_len);
aead_request_set_crypt(req, &sg, &sg, plaintext_len, iv_local);
ret = crypto_wait_req(crypto_aead_encrypt(req), &wait);
memzero_explicit(iv_local, sizeof(iv_local));
out_free_req:
aead_request_free(req);
out_free_tfm:
crypto_free_aead(tfm);
return ret;
}
| 단계 | 메모리 레이아웃 | cryptlen 값 | 주의점 |
|---|---|---|---|
| 암호화 입력 | AAD || plaintext | plaintext_len | AAD는 aead_request_set_ad()로 별도 지정합니다. |
| 암호화 출력 | AAD || ciphertext || tag | 동일 | dst 버퍼 끝에 tag 공간이 미리 있어야 합니다. |
| 복호화 입력 | AAD || ciphertext || tag | ciphertext_len + authsize | tag까지 포함한 길이를 넘겨야 합니다. |
| 복호화 출력 | AAD || plaintext | 동일 | 태그 검증 실패 시 -EBADMSG가 대표적 오류입니다. |
src와 dst를 다르게 쓴다면 dst의 앞부분 AAD를 호출자가 직접 복사해 놓아야 합니다.
그래서 실무에서는 in-place 레코드 처리가 구현도 단순하고 버그도 적습니다.
kTLS, IPsec ESP, 일부 저장 포맷이 모두 이 레이아웃 감각 위에 서 있습니다. "왜 내 tag 검증이 항상 실패하지?"라는 문제는 대개 assoclen, cryptlen, tag 길이 중 하나를 잘못 넣었을 때 발생합니다. 특히 복호화에서 ciphertext_len만 넣고 tag 길이를 빼먹는 실수가 매우 흔합니다.
HMAC과 키 파생: 공유 비밀을 바로 쓰지 않는 이유
커널 안에서 암호를 다루다 보면 "공유 비밀이 나왔으니 바로 AES 키로 써도 되는가?"라는 질문이 자주 나옵니다. 보통은 그렇지 않습니다. ECDH 결과나 장치 고유 비밀은 길이, 분포, 방향 분리 문제 때문에 곧바로 데이터 키로 쓰지 않고, HMAC 기반 KDF를 한 번 더 거칩니다. 아래 예시는 hmac(sha256) 하나만으로 HKDF와 비슷한 구조를 구현해 TX/RX 키를 분리하는 패턴입니다.
#include <crypto/hash.h>
#include <linux/slab.h>
#include <linux/string.h>
static int hmac_sha256_once(const u8 *key, unsigned int key_len,
const u8 *msg, unsigned int msg_len,
u8 out[32])
{
struct crypto_shash *tfm;
struct shash_desc *desc;
unsigned int dlen;
int ret;
tfm = crypto_alloc_shash("hmac(sha256)", 0, 0);
if (IS_ERR(tfm))
return PTR_ERR(tfm);
dlen = sizeof(*desc) + crypto_shash_descsize(tfm);
desc = kmalloc(dlen, GFP_KERNEL);
if (!desc) {
crypto_free_shash(tfm);
return -ENOMEM;
}
desc->tfm = tfm;
ret = crypto_shash_setkey(tfm, key, key_len);
if (!ret)
ret = crypto_shash_digest(desc, msg, msg_len, out);
memzero_explicit(desc, dlen);
kfree(desc);
crypto_free_shash(tfm);
return ret;
}
static int derive_tx_rx_keys_from_secret(const u8 *salt, unsigned int salt_len,
const u8 *shared, unsigned int shared_len,
const u8 *info, unsigned int info_len,
u8 tx_key[32], u8 rx_key[32])
{
u8 prk[32];
u8 t[32];
u8 *block;
int ret;
block = kmalloc(32 + info_len + 1, GFP_KERNEL);
if (!block)
return -ENOMEM;
/* Extract: PRK = HMAC(salt, shared_secret) */
ret = hmac_sha256_once(salt, salt_len, shared, shared_len, prk);
if (ret)
goto out;
/* Expand block 1: T1 = HMAC(PRK, info || 0x01) */
memcpy(block, info, info_len);
block[info_len] = 1;
ret = hmac_sha256_once(prk, sizeof(prk), block, info_len + 1, t);
if (ret)
goto out;
memcpy(tx_key, t, 32);
/* Expand block 2: T2 = HMAC(PRK, T1 || info || 0x02) */
memcpy(block, t, 32);
memcpy(block + 32, info, info_len);
block[32 + info_len] = 2;
ret = hmac_sha256_once(prk, sizeof(prk),
block, 32 + info_len + 1, rx_key);
out:
memzero_explicit(prk, sizeof(prk));
memzero_explicit(t, sizeof(t));
memzero_explicit(block, 32 + info_len + 1);
kfree(block);
return ret;
}
| 단계 | 의미 | 보안상 이점 |
|---|---|---|
| Extract | shared secret를 HMAC로 한 번 정규화해 PRK를 만듭니다. | 편향된 입력이나 길이 차이를 정리하고 salt 정책을 적용할 수 있습니다. |
| Expand block 1 | info || 0x01로 첫 번째 방향 키를 만듭니다. | 동일한 공유 비밀에서 파생되더라도 TX/RX 역할을 분리할 수 있습니다. |
| Expand block 2 | T1 || info || 0x02로 두 번째 방향 키를 만듭니다. | 키 재사용을 피하고, 반대 방향 트래픽이 같은 키를 쓰지 않게 합니다. |
crypto_rng로 DRBG를 직접 다루는 패턴
커널에서 일반적인 난수는 거의 항상 get_random_bytes()가 정답입니다. 하지만 FIPS 경계 안에서 특정 DRBG를 명시적으로 선택해야 하거나, 시험 벡터 재현처럼 랜덤 생성기 종류를 통제해야 하는 상황에서는 crypto_rng가 필요합니다.
#include <crypto/rng.h>
#include <linux/random.h>
#include <linux/string.h>
static int fill_nonce_from_drbg(u8 *out, unsigned int out_len)
{
struct crypto_rng *drbg;
u8 seed[64];
int seed_len;
int ret;
drbg = crypto_alloc_rng("drbg_nopr_hmac_sha256", 0, 0);
if (IS_ERR(drbg))
return PTR_ERR(drbg);
seed_len = crypto_rng_seedsize(drbg);
if (seed_len < 0 || seed_len > sizeof(seed)) {
ret = -EOVERFLOW;
goto out_free_rng;
}
if (seed_len) {
get_random_bytes(seed, seed_len);
ret = crypto_rng_reset(drbg, seed, seed_len);
if (ret)
goto out_zero_seed;
}
ret = crypto_rng_get_bytes(drbg, out, out_len);
out_zero_seed:
memzero_explicit(seed, sizeof(seed));
out_free_rng:
crypto_free_rng(drbg);
return ret;
}
| 언제 쓰나 | 권장 API | 이유 |
|---|---|---|
| 일반 nonce, cookie, 임시 키 | get_random_bytes() | 커널 기본 CSPRNG를 가장 단순하고 안전하게 사용합니다. |
| 특정 DRBG 정책 강제 | crypto_alloc_rng() | 알고리즘 이름과 seed 정책을 명시할 수 있습니다. |
| 자가 테스트/재현 실험 | crypto_rng_reset() | 같은 seed로 같은 경로를 반복 검증하기 쉽습니다. |
get_random_bytes()로 seed를 받아 DRBG를 초기화하는 것이 보통이며, seed와 내부 상태는 가능한 한 빨리 소거해야 합니다.
압축 API: crypto_comp와 crypto_acomp
Crypto Framework는 암호화뿐 아니라 압축도 함께 다룹니다. 이유는 커널 관점에서 "가속 가능한 데이터 변환"이라는 공통점이 크기 때문입니다. 특히 SoC 압축 엔진이나 PCI 가속기를 붙이는 드라이버는 암호화와 동일한 방식으로 algorithm 등록, request 큐잉, callback 완료를 구현합니다.
#include <linux/crypto.h>
#include <crypto/acompress.h>
#include <linux/scatterlist.h>
static int compress_lz4_sync(const u8 *src, unsigned int src_len,
u8 *dst, unsigned int *dst_len)
{
struct crypto_comp *tfm;
int ret;
tfm = crypto_alloc_comp("lz4", 0, 0);
if (IS_ERR(tfm))
return PTR_ERR(tfm);
ret = crypto_comp_compress(tfm, src, src_len, dst, dst_len);
crypto_free_comp(tfm);
return ret;
}
static int compress_deflate_async(struct scatterlist *src,
struct scatterlist *dst,
unsigned int src_len,
unsigned int *dst_len)
{
struct crypto_acomp *tfm;
struct acomp_req *req;
int ret;
DECLARE_CRYPTO_WAIT(wait);
tfm = crypto_alloc_acomp("deflate", 0, 0);
if (IS_ERR(tfm))
return PTR_ERR(tfm);
req = acomp_request_alloc(tfm);
if (!req) {
ret = -ENOMEM;
goto out_free_tfm;
}
acomp_request_set_callback(req,
CRYPTO_TFM_REQ_MAY_SLEEP | CRYPTO_TFM_REQ_MAY_BACKLOG,
crypto_req_done, &wait);
acomp_request_set_params(req, src, dst, src_len, *dst_len);
ret = crypto_wait_req(crypto_acomp_compress(req), &wait);
if (!ret)
*dst_len = req->dlen;
acomp_request_free(req);
out_free_tfm:
crypto_free_acomp(tfm);
return ret;
}
| 구분 | 특징 | 어울리는 상황 |
|---|---|---|
crypto_comp | 동기, 연속 버퍼 기반 | 짧은 제어 경로, 작은 메타데이터, 간단한 소프트웨어 압축 |
crypto_acomp | 비동기, SG 기반, callback 가능 | DMA 압축 엔진, 긴 데이터 스트림, 하드웨어 offload |
압축 API를 굳이 Crypto Framework에 넣은 이유도 여기서 드러납니다. 상위 계층 입장에서는 "데이터 조각을 SG로 넘기고, 나중에 완료를 통지받는 변환 작업"이라는 점에서 암호화와 구조가 거의 같습니다. 그래서 storage stack이나 net stack이 암호 엔진과 압축 엔진을 비슷한 틀로 감쌀 수 있습니다.
펌웨어 검증 파이프라인: hash와 sig를 분리해서 조립
실제 검증 코드는 대개 "메시지 본문을 해시"한 뒤, 그 digest를 "서명 API로 검증"합니다. Crypto API의 crypto_sig는 이 경계를 분명히 드러내기 때문에, 검증 로직을 구현할 때도 해시 단계와 서명 단계가 자연스럽게 분리됩니다.
#include <crypto/hash.h>
#include <crypto/sig.h>
#include <linux/string.h>
static int verify_firmware_blob(const u8 *image, unsigned int image_len,
const u8 *sig, unsigned int sig_len,
const u8 *pubkey_der, unsigned int pubkey_der_len)
{
struct crypto_sig *tfm;
u8 digest[32];
int ret;
ret = calc_sha256(image, image_len, digest);
if (ret)
return ret;
tfm = crypto_alloc_sig("ecdsa", 0, 0);
if (IS_ERR(tfm)) {
memzero_explicit(digest, sizeof(digest));
return PTR_ERR(tfm);
}
ret = crypto_sig_set_pubkey(tfm, pubkey_der, pubkey_der_len);
if (!ret)
ret = crypto_sig_verify(tfm, sig, sig_len, digest, sizeof(digest));
memzero_explicit(digest, sizeof(digest));
crypto_free_sig(tfm);
return ret;
}
| 단계 | 역할 | 실수 포인트 |
|---|---|---|
| 해시 | 본문을 고정 길이 digest로 축약합니다. | 서명 알고리즘과 해시 정책을 상위 포맷에서 어떻게 선언하는지 놓치기 쉽습니다. |
| 공개키 설정 | DER/BER 인코딩된 공개키를 tfm에 적재합니다. | 키 인코딩 형식이 맞지 않으면 알고리즘 자체는 정상이어도 검증이 실패합니다. |
| 서명 검증 | digest와 서명값을 비교합니다. | 본문 전체를 넣는 것이 아니라 digest를 넣는 API라는 점을 자주 혼동합니다. |
crypto_engine 기반 하드웨어 큐 드라이버 패턴
가속기 드라이버를 직접 작성할 때 가장 흔한 함정은 "요청 큐, 락, IRQ 완료, backlog 깨우기"를 전부 손으로 구현하려다가 request 수명 버그를 만드는 것입니다. 이때 crypto_engine은 Crypto API request를 디바이스 큐에 안전하게 넘기고, 완료 시 상위 계층으로 되돌리는 공통 뼈대를 제공합니다.
#include <crypto/engine.h>
#include <crypto/skcipher.h>
struct my_dev {
struct skcipher_request *active_req;
};
struct my_tfm_ctx {
struct crypto_engine *engine;
struct my_dev *dd;
};
static int my_engine_do_one_request(struct crypto_engine *engine, void *areq)
{
struct crypto_async_request *base = areq;
struct skcipher_request *req =
container_of(base, struct skcipher_request, base);
struct crypto_skcipher *tfm = crypto_skcipher_reqtfm(req);
struct my_tfm_ctx *ctx = crypto_skcipher_ctx(tfm);
(void)engine;
ctx->dd->active_req = req;
/* 여기서 SG를 DMA 매핑하고, 레지스터를 적재한 뒤 HW 시작 */
/* 예시는 설명을 위해 단일 active request만 추적합니다. */
/* 완료는 IRQ에서 crypto_finalize_skcipher_request()로 통지 */
return -EINPROGRESS;
}
static int my_skcipher_encrypt(struct skcipher_request *req)
{
struct crypto_skcipher *tfm = crypto_skcipher_reqtfm(req);
struct my_tfm_ctx *ctx = crypto_skcipher_ctx(tfm);
return crypto_transfer_skcipher_request_to_engine(ctx->engine, req);
}
static irqreturn_t my_irq(int irq, void *data)
{
struct my_dev *dd = data;
struct my_tfm_ctx *ctx;
int err = my_hw_status(dd);
ctx = crypto_skcipher_ctx(crypto_skcipher_reqtfm(dd->active_req));
/* DMA unmap, 상태 비트 정리 후 상위 요청 완료 통지 */
crypto_finalize_skcipher_request(ctx->engine, dd->active_req, err);
dd->active_req = NULL;
return IRQ_HANDLED;
}
| 역할 | 핵심 함수 | 설명 |
|---|---|---|
| 요청 큐 입력 | crypto_transfer_skcipher_request_to_engine() | 상위 request를 엔진 큐에 넣고 backlog/순서를 관리합니다. |
| 하드웨어 시작 | do_one_request 콜백 | 드라이버가 DMA 매핑, 레지스터 설정, HW kick을 수행합니다. |
| 완료 통지 | crypto_finalize_skcipher_request() | IRQ/bottom-half에서 상위 Crypto API request를 깨웁니다. |
| 엔진 수명 | crypto_engine_alloc_init(), crypto_engine_start() | probe/remove 수명과 같이 관리합니다. |
prepare_cipher_request/unprepare_cipher_request, runtime PM, DMA 오류 복구까지 함께 다룹니다.
핵심은 request 자체를 상위 API가 소유하고, 드라이버는 그 request를 "빌려서" HW에 태우는 구조라는 점입니다. 따라서 드라이버는 request를 복제하려 들기보다, reqctx에 DMA 매핑 상태나 디스크립터 포인터만 저장하고 완료 후 정리하는 방식이 맞습니다. cra_ctxsize가 tfm 단위 상태라면, crypto_skcipher_set_reqsize()로 잡는 공간은 in-flight 작업 단위 상태입니다.
암호화 성능 추적과 벤치마크
커널 암호화 작업의 성능 분석에는 tcrypt 모듈 벤치마크, ftrace 함수 추적, bpftrace 동적 계측의 세 가지 기법이 주로 사용됩니다. 이 섹션에서는 각 도구의 실전 활용법을 다룹니다.
tcrypt 고급 벤치마크
tcrypt 모듈은 커널 내부에서 직접 Crypto Framework (Crypto API)를 호출하여 순수 암호 연산 성능을 측정합니다. I/O 스택 오버헤드 없이 알고리즘 자체의 throughput을 확인할 수 있습니다:
# ═══ tcrypt 벤치마크 실전 활용 ═══
# skcipher 벤치마크 (AES-CBC, AES-CTR, AES-XTS)
modprobe tcrypt mode=500 sec=3 # 3초간 측정
modprobe tcrypt mode=501 sec=3 # AES-CTR
modprobe tcrypt mode=502 sec=3 # AES-XTS
# AEAD 벤치마크 (AES-GCM, ChaCha20-Poly1305)
modprobe tcrypt mode=211 sec=3 # AES-GCM
# 해시 벤치마크
modprobe tcrypt mode=403 sec=3 # SHA-256
modprobe tcrypt mode=404 sec=3 # SHA-512
# 결과 파싱: ops/sec와 throughput 추출
dmesg | grep "testing speed" -A 20 | tail -25
# testing speed of async cbc(aes) (cbc-aes-aesni) encryption
# test 5 (256 bit key, 8192 byte blocks): 495230 ops in 3 sec
# → 495230 × 8192 / 3 = ~1.35 GB/s
# S/W vs H/W 비교: aes_generic 강제 사용
# 1. aesni_intel 모듈 일시 언로드
modprobe -r aesni_intel 2>/dev/null
modprobe tcrypt mode=500 sec=3 # aes_generic으로 테스트
# 2. aesni_intel 재로드
modprobe aesni_intel
modprobe tcrypt mode=500 sec=3 # AES-NI로 테스트
# → throughput 비교로 가속 비율 확인
ftrace를 이용한 암호화 함수 추적
ftrace는 커널 내장 추적기로, 암호화 함수의 호출 빈도와 지연 시간을 분석할 수 있습니다:
# ═══ ftrace로 crypto 함수 추적 ═══
# 1. 추적 가능한 crypto 함수 목록 확인
grep crypto /sys/kernel/debug/tracing/available_filter_functions | head -20
# crypto_skcipher_encrypt
# crypto_skcipher_decrypt
# crypto_aead_encrypt
# crypto_shash_digest
# crypto_alloc_tfm
# 2. function_graph 추적기로 호출 흐름 확인
cd /sys/kernel/debug/tracing
echo function_graph > current_tracer
echo crypto_skcipher_encrypt > set_graph_function
echo 1 > tracing_on
# dm-crypt I/O 발생 (다른 터미널에서)
dd if=/dev/urandom of=/dev/mapper/my-crypt bs=4K count=100
echo 0 > tracing_on
cat trace | head -40
# kcryptd/0-1234 | 0.456 us | crypto_skcipher_encrypt();
# | | cbc_encrypt();
# | | kernel_fpu_begin();
# | 0.312 us | aesni_cbc_enc();
# | | kernel_fpu_end();
# 3. 함수 호출 히스토그램 (지연 시간 분포)
echo 0 > tracing_on
echo nop > current_tracer
echo crypto_skcipher_encrypt > set_ftrace_filter
echo 1 > function_profile_enabled
echo 1 > tracing_on
# 워크로드 실행 후
cat trace_stat/function0 | grep crypto
# Function Hit Time Avg
# -------- --- ---- ---
# crypto_skcipher_encrypt 152847 45.230 ms 0.296 us
# 4. 추적 해제
echo 0 > tracing_on
echo 0 > function_profile_enabled
echo nop > current_tracer
echo > set_ftrace_filter
bpftrace를 이용한 동적 계측
bpftrace는 eBPF 기반의 고급 추적 도구로, 커널 암호화 작업의 세부 통계를 실시간(Real-time)으로 수집할 수 있습니다:
# ═══ bpftrace로 crypto 연산 계측 ═══
# 1. 암호화 함수 호출 빈도 (10초간 수집)
bpftrace -e '
kprobe:crypto_skcipher_encrypt,
kprobe:crypto_skcipher_decrypt,
kprobe:crypto_aead_encrypt,
kprobe:crypto_aead_decrypt,
kprobe:crypto_shash_digest {
@calls[probe] = count();
}
interval:s:10 { exit(); }
'
# @calls[kprobe:crypto_skcipher_encrypt]: 284713
# @calls[kprobe:crypto_aead_encrypt]: 15234
# @calls[kprobe:crypto_shash_digest]: 42891
# 2. 암호화 함수별 지연 시간 히스토그램
bpftrace -e '
kprobe:crypto_skcipher_encrypt { @start[tid] = nsecs; }
kretprobe:crypto_skcipher_encrypt /@start[tid]/ {
@latency_ns = hist(nsecs - @start[tid]);
delete(@start[tid]);
}
interval:s:10 { exit(); }
'
# @latency_ns:
# [128, 256) 12834 |@@@@@@@@@@@@@@@@@@@@|
# [256, 512) 85219 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
# [512, 1K) 42103 |@@@@@@@@@@@@@@@@@@@|
# [1K, 2K) 5847 |@@|
# [2K, 4K) 312 |
# 3. 프로세스별 crypto 사용량 추적
bpftrace -e '
kprobe:crypto_skcipher_encrypt {
@by_comm[comm] = count();
}
interval:s:10 { exit(); }
'
# @by_comm[kcryptd/0]: 198234
# @by_comm[ksoftirqd/2]: 45123
# @by_comm[openvpn]: 12345
# 4. kernel_fpu_begin/end 구간 측정 (FPU 점유 시간)
bpftrace -e '
kprobe:kernel_fpu_begin_mask { @fpu_start[tid] = nsecs; }
kprobe:kernel_fpu_end /@fpu_start[tid]/ {
@fpu_hold_ns = hist(nsecs - @fpu_start[tid]);
delete(@fpu_start[tid]);
}
interval:s:5 { exit(); }
'
# → FPU 점유 시간이 너무 길면 preemption latency 문제 가능
perf를 이용한 crypto 프로파일링(Profiling)
# ═══ perf로 crypto 핫스팟 분석 ═══
# 1. dm-crypt I/O 중 CPU 프로파일
perf record -g -p $(pgrep -f kcryptd) -- sleep 10
perf report --no-children
# 주요 확인 포인트:
# aesni_xts_encrypt → AES-NI 사용 확인
# aes_encrypt → S/W fallback 사용 (성능 문제)
# kernel_fpu_begin → FPU 컨텍스트 스위칭 비용
# scatterwalk_* → scatterlist 순회 오버헤드
# 2. 특정 crypto 함수의 캐시 미스 분석
perf stat -e cache-misses,cache-references,instructions \
-p $(pgrep -f kcryptd) -- sleep 5
# 3. IPsec crypto 성능 분석
perf top -e cycles -g --no-children \
--call-graph dwarf -- -p $(pgrep -f "pluto\|charon")
# 4. CPU 명령어별 통계 (AES-NI 파이프라인 효율)
perf stat -e cycles,instructions,r01b1,r02b1 \
-- modprobe tcrypt mode=500 sec=2
# r01b1: UOPS_EXECUTED.THREAD (마이크로-ops 실행 수)
# → IPC(Instructions Per Cycle)로 파이프라인 효율 확인
성능 추적 실전 팁: (1) tcrypt는 모듈 로드 시 즉시 테스트를 수행하고 완료 후 -EAGAIN을 반환하므로 rmmod tcrypt로 정리 불필요, (2) ftrace의 function_graph 추적은 오버헤드가 크므로 프로덕션에서는 짧은 시간만 사용, (3) bpftrace는 오버헤드가 매우 낮아(<1%) 프로덕션 서버에서도 안전하게 사용 가능, (4) 벤치마크 시 터보 부스트(cpupower frequency-set -g performance)와 NUMA 바인딩(numactl --cpunodebind=0)으로 측정 변동성을 줄이세요.
관련 문서
암호화 드라이버 구현과 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.
외부 참고 자료
- Developing Cipher Algorithms — 커널 암호 알고리즘 개발 가이드입니다
- Crypto API Architecture — Crypto API 내부 아키텍처 상세 문서입니다
- tcrypt.c — 커널 암호 벤치마크 모듈 소스입니다
- testmgr.c — 커널 암호 테스트 매니저 소스입니다
- Linux 커널 crypto/ 디렉토리 — 커널 암호화 서브시스템 소스 코드입니다
- NIST CAVP — 암호 알고리즘 검증 프로그램 (FIPS 인증 테스트)입니다