פתרון זהות והצפנת תעבורה¶
פתרון התרגיל הראשון¶
נשתמש בלולאה כללית שאינה מניחה איך התעבורה ממומשת. הפונקציה send_bytes חייבת לשלוח את כל הspan או להיכשל; הפונקציה receive מחזירה מקטע חדש בבעלותה, וריק פירושו סוף transport. שתי הפונקציות הן adapters שאנחנו מחברים ללולאות השליחה והקריאה שכבר כתבנו. בגרסה אסינכרונית אותו סדר פעולות הופך למכונת מצבים, ולא לקריאה חוסמת מתוך worker של IOCP.
הקוד הבא מצטרף לtls_handshake.h ומכיל את כל לוגיקת הbuffer של הhandshake. הוא אינו מיישם סיום TLS או פרוטוקול יישום.
#include <algorithm>
#include <functional>
std::vector<std::byte> run_handshake(
tls_handshake& tls,
const std::function<void(std::span<const std::byte>)>& send_bytes,
const std::function<std::vector<std::byte>()>& receive) {
std::vector<std::byte> input;
bool need_read = false;
for (unsigned iteration = 0; iteration < 256; ++iteration) {
if (need_read) {
auto received = receive();
if (received.empty()) throw std::runtime_error("transport ended during TLS handshake");
if (received.size() > 256 * 1024 - input.size())
throw std::runtime_error("TLS input budget exceeded");
input.insert(input.end(), received.begin(), received.end());
}
auto step = tls.advance(input);
if (!step.output.empty()) send_bytes(step.output);
if (step.consumed > input.size()) throw std::logic_error("invalid consumption");
input.erase(input.begin(), input.begin() + static_cast<std::ptrdiff_t>(step.consumed));
if (step.progress == tls_progress::ready) return input;
need_read = step.progress == tls_progress::more_input || input.empty();
if (!need_read && step.consumed == 0 && step.output.empty())
throw std::runtime_error("handshake made no progress");
}
throw std::runtime_error("handshake step budget exceeded");
}
הצעד הראשון מופעל עם input ריק ושולח בדרך כלל את הtoken הראשון. כאשר יש more_input עם קלט קיים, איננו מוחקים אותו; מוסיפים את המקטע הבא. כאשר הhandshake הושלם עם extra, מחזירים את היתרה לשכבת פענוח הנתונים.
מגבלת צעדים ומגבלת bytes אינן deadline. הadapter החוסם צריך לקבל תקציב זמן מתאים, ובשרת אסינכרוני נשמור timer של השיחה וננקז I/O בזמן סגירה. נבחין בין החלטה להפסיק את השיחה לבין האפשרות לשחרר buffer שהרשת עדיין משתמשת בו.
בבדיקה עם מנוע מדומה, למשל קלט של עשרה bytes וצריכה של שישה, נדרוש שבקריאה הבאה יופיעו בדיוק ארבעת האחרונים. בהצלחה עם extra נוודא שההחזרה מכילה את היתרה גם כאשר נוצר באותו צעד output נוסף שעדיין היה צריך לשלוח.
פתרון התרגיל השני¶
התעודה התקינה צריכה לאפשר השלמה תחת מדיניות האמון והשם של המעבדה. תעודה לשם אחר, תוקף לא מתאים או שרשרת לא מהימנה צריכות להיכשל באימות. כשל revocation יכול לנבוע גם מחוסר יכולת להשיג מידע נדרש; נשמור את ההבחנה באבחון ולא נכנה כל כשל "שרת מזויף".
נחבר מונה לשליחת payload עסקי ונדרוש שיישאר אפס בכל מסלול שלא הגיע לready. הtokens של הhandshake עצמם כמובן נשלחים לפני ready; הבדיקה מבחינה בינם לבין סוד או פקודה של היישום.
נבדוק גם את אותו שרת דרך שם צפוי שונה בלי לשנות את כתובת החיבור. כך נפריד בין היכולת להגיע לשרת לבין השאלה אם תעודתו מתאימה לזהות שביקשנו. לא נשנה את target_ לפי התעודה שהשרת הציג.
פתרון התרגיל השלישי¶
נוסיף את הפונקציה הבאה ליד מחלקת הhandshake. היא מקבלת span של bytes ומחזירה vector שמכיל את הrecords להצפנה שהופקו. השימוש בה חייב להיות מסודר ביחס לפעולות הצפנה אחרות של אותו context.
#include <cstring>
std::vector<std::byte> encrypt_record(CtxtHandle* context,
std::span<const std::byte> plain) {
SecPkgContext_StreamSizes sizes{};
SECURITY_STATUS status = QueryContextAttributesW(context, SECPKG_ATTR_STREAM_SIZES, &sizes);
if (status != SEC_E_OK) throw std::runtime_error("query stream sizes failed");
if (plain.size() > sizes.cbMaximumMessage)
throw std::invalid_argument("plaintext exceeds one TLS message");
auto total = static_cast<std::size_t>(sizes.cbHeader) + plain.size() + sizes.cbTrailer;
std::vector<std::byte> storage(total);
if (!plain.empty()) std::memcpy(storage.data() + sizes.cbHeader, plain.data(), plain.size());
SecBuffer buffers[4] = {
{sizes.cbHeader, SECBUFFER_STREAM_HEADER, storage.data()},
{static_cast<ULONG>(plain.size()), SECBUFFER_DATA, storage.data() + sizes.cbHeader},
{sizes.cbTrailer, SECBUFFER_STREAM_TRAILER, storage.data() + sizes.cbHeader + plain.size()},
{0, SECBUFFER_EMPTY, nullptr}
};
SecBufferDesc desc{SECBUFFER_VERSION, 4, buffers};
status = EncryptMessage(context, 0, &desc, 0);
if (status != SEC_E_OK) throw std::runtime_error("TLS encryption failed");
std::vector<std::byte> wire;
for (unsigned i = 0; i != 3; ++i) {
if (buffers[i].cbBuffer == 0) continue;
auto first = static_cast<const std::byte*>(buffers[i].pvBuffer);
wire.insert(wire.end(), first, first + buffers[i].cbBuffer);
}
return wire;
}
ההעתקה לwire משתמשת במצביע ובאורך שהוחזרו לכל buffer, ולא מניחה שכל גודל נשאר בדיוק כפי שהיה לפני ההצפנה. ביישום שמנסה לצמצם העתקות אפשר להחזיק את האחסון ואת רשימת הbuffers יחד, אבל צריך לשמור על אותו חוזה בעלות.
אחרי הגשה אסינכרונית, התוצאה שייכת לפעולת השליחה עד completion הסופי שלה. פקיעת זמן או סגירה לוגית אינן משחררות אותה. אם השליחה חלקית, נשמור offset ונמשיך לפי המידע שהושלם.
בפענוח נפעיל DecryptMessage על אחסון מוצפן שבבעלותנו. אחרי הצלחה נסיים לצרוך או נעתיק את הplaintext לפני הזזת הextra לתחילת הbuffer. במצב SEC_E_INCOMPLETE_MESSAGE נשמור את הקלט ונוסיף מידע. במצב SEC_I_CONTEXT_EXPIRED נטפל בסיום TLS, ובמצב שדורש המשך handshake נעביר שליטה למצב מתאים או נדחה באופן מפורש אם המוצר לא תומך בו. לא נפרש שום סטטוס לא מוכר כהצלחה עם אפס bytes.