تزریق الگوریتم های اختصاصی

در زمان نصب os بر روی روتر بعد از اینکه ایمیج SoodarOS را بر روی یک فلش bootable کردید قبل از نصب باید الگوریتم ها را در مسیر مشخص قرار دهید .

یک پوشه به نام data بسازید و سپس داخل آن یک پوشه دیگر به نام algs بسازید و الگوریتم های خود را داخل آن کپی نمایید و سپس ایمیج را روی دستگاه نصب کنید .

فعال کردن الگوریتم اختصاصی

برای فعال کردن الگوریتم های اختصاصی باید از دستور زیر در cli روتر استفاده کنید و روتر را reload کنید پس از آن از الگوریتم های اختصاصی شما در روتر استفاده خواهد شد :

Soodar/config# crypto engine custom tls

گزینه tls اختیاری است و اگر در دستور tls را هم وارد کنید tls هم از الگوریتم های اختصاصی استفاده می کند .

برای برگشت به حالت استاندارد و استفاده از الگوریتم های استاندارد باید دستور زیر را وارد کنید و سپس روتر را reload کنید :

Soodar/config# crypto engine conventional

پیاده سازی الگوریتم رمزنگاری اختصاصی

در سودار مصرف کننده میتواند برای الگوریتم‌های رمزنگاری مشخص شده، پیاده سازی اختصاصی خود را ارائه کند. این پیاده سازی به شکل یک کتابخانه‌ی اشتراکی لینوکسی (shared object) ساخته میشود و سودار آن را در زمان اجرا به عنوان plugin بارگذاری میکند. پس از بارگذاری موفق، سرویس‌ها و برنامه‌هایی که از آن الگوریتم استفاده میکنند بدون نیاز به تغییر در تنظیمات عملیاتی خود، از پیاده سازی اختصاصی استفاده خواهند کرد.

الگوریتم‌های قابل پیاده سازی عبارتند از:

  • AES-128 در حالت‌های CBC، ECB، CTR و GCM

  • AES-192 در حالت‌های CBC، ECB، CTR و GCM

  • AES-256 در حالت‌های CBC، ECB، CTR و GCM

  • ChaCha20-Poly1305

این سند قرارداد فنی بین سودار و کتابخانه‌ی اختصاصی را توضیح میدهد. نام، نوع ورودی‌ها، نوع خروجی و رفتار توابع باید دقیقا مطابق همین قرارداد باشد.

اصول کلی پیاده سازی

کتابخانه‌ی اختصاصی باید یک فایل so. معتبر برای لینوکس باشد و توابع مورد نیاز را با نام دقیق مشخص شده صادر کند. اگر کتابخانه با ++C پیاده سازی میشود، باید از extern "C" برای جلوگیری از name mangling استفاده شود تا سودار بتواند نمادهای مورد نیاز را با همان نام پیدا کند.

تمام توابعی که خروجی عددی دارند باید در صورت موفقیت مقدار 0 برگردانند. هر مقدار غیر از 0 به معنی شکست عملیات است. در صورت شکست، تابع نباید خروجی ناقص یا نامعتبر را به عنوان خروجی قابل استفاده در اختیار سودار قرار دهد.

پیاده سازی باید حداقل نکات زیر را رعایت کند:

  • تمام وضعیت داخلی الگوریتم باید در context نگهداری شود.

  • هر فراخوانی init باید یک context مستقل بسازد.

  • تابع cleanup باید تمام حافظه و منابع مربوط به همان context را آزاد کند.

  • کلید، nonce، شمارنده و وضعیت داخلی نباید بین contextهای مختلف به اشتراک گذاشته شوند.

  • طول کلید باید دقیقا با الگوریتم انتخاب شده منطبق باشد.

  • بافرهای ورودی و خروجی باید به صورت باینری در نظر گرفته شوند؛ داده‌ها رشته‌ی متنی نیستند و ممکن است شامل بایت صفر باشند.

  • در صورت نامعتبر بودن context، کلید، nonce، طول داده یا هر پارامتر ضروری دیگر، تابع باید خطا برگرداند.

هدرهای لازم

در امضای توابع از size_t و uint64_t استفاده میشود. بنابراین پیاده سازی C یا ++C باید هدرهای لازم را وارد کند:

#include <stddef.h>
#include <stdint.h>

تابع init

این تابع یک context جدید برای عملیات رمزنگاری میسازد و اشاره گر آن را برمیگرداند.

void *init(void);

خروجی تابع همان context ساخته شده است. اگر ساخت context ممکن نباشد، تابع باید مقدار NULL برگرداند. پیاده سازی باید تمام فیلدهای داخلی context را به مقدار اولیه‌ی مشخص و امن تنظیم کند تا قبل از دریافت کلید یا پارامترهای لازم، وضعیت نامشخص ایجاد نشود.

تابع set_key

این تابع کلید الگوریتم را روی context تنظیم میکند.

int set_key(void *context, const unsigned char *key);

پارامتر context همان مقداری است که از init دریافت شده است. پارامتر key کلید خام و باینری الگوریتم است. طول کلید در این امضا ارسال نمیشود؛ بنابراین پیاده سازی باید طول کلید مورد انتظار را بر اساس الگوریتمی که برای آن ساخته شده است بداند و دقیقا همان تعداد بایت را بخواند.

طول کلیدها به شکل زیر است:

  • برای AES-128: طول کلید 16 بایت است.

  • برای AES-192: طول کلید 24 بایت است.

  • برای AES-256: طول کلید 32 بایت است.

  • برای ChaCha20-Poly1305: طول کلید 32 بایت است.

اگر context یا key نامعتبر باشد، یا اگر پیاده سازی نتواند ساختار داخلی کلید را آماده کند، تابع باید مقداری غیر از 0 برگرداند.

تایع set_nonce

این تابع nonce را تنظیم میکند و فقط برای chacha20 استفاده می شود . تعریف این تابع به شکل زیر میباشد:

int set_nonce(void *context, const unsigned char *nonce, size_t nonce_len)

در این تابع، پارامتر context همان Context ایجاد شده توسط تابع init میباشد. پارامتر nonce مقدار nonce میباشد. خروجی این تابع یک عدد است که 0 بیانگر عملیات موفق و هر عددی غیر از آن بیانگر عملیات ناموفق میباشد.

تابع تابع set_counter

این تابع counter را تنظیم میکند و فقط برای chacha20 استفاده می شود .تعریف این تابع به شکل زیر میباشد:

int set_counter(void *context, uint64_t counter)

در این تابع، پارامتر context همان Context ایجاد شده توسط تابع init میباشد. پارامتر counter مقدار nonce میباشد. خروجی این تابع یک عدد است که 0 بیانگر عملیات موفق و هر عددی غیر از آن بیانگر عملیات ناموفق میباشد.

تابع decrypt برای AES

برای پیاده سازی‌های AES، تابع رمزگشایی نیز روی یک بلوک 16 بایتی کار میکند.

int decrypt(void *context, const unsigned char *input, unsigned char *output);

پارامتر input یک بلوک رمز شده‌ی 16 بایتی است و پارامتر output باید فضای کافی برای 16 بایت خروجی داشته باشد. خروجی، بلوک فاش متناظر با ورودی است.

این تابع باید فقط بعد از تنظیم موفق کلید استفاده شود. اگر context یا کلید داخلی آماده نباشد، باید خطا برگردانده شود.

تابع encrypt برای ChaCha20-Poly1305

برای ChaCha20-Poly1305 ورودی و خروجی طول متغیر دارند و طول داده با پارامتر len ارسال میشود.

int encrypt(void *context, const unsigned char *input, unsigned char *output, size_t len);

پارامتر input به داده‌ی فاش اشاره میکند و output باید حداقل len بایت فضا داشته باشد. تابع باید دقیقا len بایت خروجی تولید کند. در ChaCha20 عملیات رمزنگاری و رمزگشایی هر دو با اعمال جریان کلید روی داده انجام میشوند، اما پیاده سازی باید همچنان دو تابع جداگانه‌ی encrypt و decrypt را با نام‌های مورد انتظار صادر کند.

قبل از فراخوانی این تابع، کلید باید با set_key تنظیم شده باشد. همچنین nonce و در صورت نیاز counter باید با set_nonce و set_counter در context قرار گرفته باشند.

تابع decrypt برای ChaCha20-Poly1305

برای ChaCha20-Poly1305 تابع رمزگشایی نیز طول داده را به صورت صریح دریافت میکند.

int decrypt(void *context, const unsigned char *input, unsigned char *output, size_t len);

پارامتر input داده‌ی رمز شده و پارامتر output محل قرار گرفتن داده‌ی فاش است. طول هر دو بافر باید حداقل len بایت باشد. تابع باید در صورت موفقیت دقیقا len بایت را پردازش کند و مقدار 0 برگرداند.

در قرارداد بالادستی احراز اصالت Poly1305 خارج از این interface انجام میشود، این تابع فقط بخش پردازش جریان داده را انجام میدهد.

تابع test

این تابع test vectorهای الگوریتم را اجرا میکند تا صحت پیاده سازی روی دستگاه فعلی بررسی شود.

int test(void);

تابع باید در صورت موفقیت همه‌ی آزمون‌ها مقدار 0 برگرداند. هرگونه اختلاف با خروجی مورد انتظار باید باعث خروجی غیر صفر شود. پیشنهاد میشود آزمون‌ها حداقل موارد زیر را پوشش دهند:

  • کلیدهای صفر و غیر صفر

  • ورودی صفر، ورودی غیر صفر و ورودی با الگوی تکراری

  • برای AES، test vectorهای رسمی همان اندازه‌ی کلید پیاده سازی شده

  • برای ChaCha20، ترکیب‌های مشخص از کلید، nonce، شمارنده و متن ورودی

  • رمزنگاری و سپس رمزگشایی برای اطمینان از برگشت پذیری

  • خطاهای پارامتری مهم، مانند نبودن کلید یا طول نامعتبر nonce

اجرای test نباید وضعیت contextهای فعال دیگر را تغییر دهد. بهتر است این تابع contextهای داخلی مورد نیاز خود را بسازد و در پایان آزاد کند.

تابع get_params

این تابع طول کلید و اندازه‌ی واحد پردازش الگوریتم را برمیگرداند.

void get_params(int *key_size, int *block_size);

پارامتر key_size باید طول کلید را بر حسب بایت دریافت کند. پارامتر block_size باید اندازه‌ی واحد پردازش را بر حسب بایت دریافت کند.

مقادیر مورد انتظار:

  • برای AES-128: مقدار key_size برابر 16 و مقدار block_size برابر 16 است.

  • برای AES-192: مقدار key_size برابر 24 و مقدار block_size برابر 16 است.

  • برای AES-256: مقدار key_size برابر 32 و مقدار block_size برابر 16 است.

  • برای ChaCha20-Poly1305: مقدار key_size برابر 32 است. مقدار block_size باید مطابق قرارداد داخلی سودار برای pluginهای stream تنظیم شود. اگر قرارداد جداگانه‌ای تعیین نشده باشد، مقدار 1 نشان میدهد که داده به صورت بایتی و با طول متغیر پردازش میشود.

اگر یکی از اشاره گرهای ورودی NULL باشد، پیاده سازی نباید باعث crash شود. در چنین حالتی فقط اشاره گر معتبر مقداردهی شود.

تابع cleanup

این تابع context ساخته شده توسط init را از بین میبرد.

void cleanup(void *context);

تابع باید تمام حافظه و منابع مربوط به context را آزاد کند. اگر در context کلید، زیرکلیدها، nonce یا وضعیت حساس دیگری ذخیره شده است، بهتر است قبل از آزادسازی حافظه، این داده‌ها پاک شوند.

فراخوانی cleanup با مقدار NULL نباید باعث crash شود.

نمونه‌ی اسکلت برای خروجی C

نمونه‌ی زیر فقط ساختار کلی توابع را نشان میدهد و پیاده سازی الگوریتم نیست:

#include <stddef.h>
#include <stdint.h>

void *init(void)
{
    return NULL;
}

int set_key(void *context, const unsigned char *key)
{
    return 1;
}

int set_nonce(void *context, const unsigned char *nonce, size_t nonce_len)
{
    return 1;
}

int set_counter(void *context, uint64_t counter)
{
    return 1;
}

int test(void)
{
    return 1;
}

void get_params(int *key_size, int *block_size)
{
    if (key_size != NULL) {
        *key_size = 0;
    }

    if (block_size != NULL) {
        *block_size = 0;
    }
}

void cleanup(void *context)
{
    (void)context;
}

برای AES باید توابع encrypt و decrypt با امضای بلوکی AES صادر شوند. برای ChaCha20-Poly1305 باید توابع encrypt و decrypt با امضای دارای len صادر شوند. یک plugin نباید همزمان چند امضای متفاوت برای یک نماد واحد صادر کند؛ برای هر خانواده‌ی الگوریتم، کتابخانه باید مطابق همان قراردادی ساخته شود که سودار برای آن الگوریتم بارگذاری میکند.

نکات امنیتی و سازگاری

پیاده سازی اختصاصی بخشی از مسیر امنیتی سیستم است و خطا در آن میتواند باعث از دست رفتن محرمانگی، صحت داده یا سازگاری سرویس‌ها شود. قبل از استفاده عملیاتی، موارد زیر باید بررسی شوند:

  • خروجی الگوریتم با test vectorهای معتبر و شناخته شده مطابقت داشته باشد.

  • مدیریت کلید و حافظه باعث نشت داده‌ی حساس نشود.

  • استفاده‌ی دوباره از nonce و counter در حالت‌هایی که نباید تکرار شوند رخ ندهد.

  • رفتار تابع‌ها در ورودی‌های نامعتبر مشخص و قابل پیش بینی باشد.

  • کتابخانه با ABI و معماری همان نسخه‌ی سودار سازگار باشد.

  • نمادهای مورد نیاز با نام دقیق و بدون تغییر صادر شده باشند.