קבלת פנים לבבית ל־ASN.1 ול־DER

המסמך מספק היכרות נעימה למבני הנתונים והתצורות שמגדירות את האישורים בהם משתמשים ב־HTTPS. הוא אמור להיות מזמין לכל אחד אפילו עם ניסיון מועט במדעי המחשב ומעט היכרות עם אישורי אבטחה.

אישור HTTPS הוא סוג של קובץ, כמו כל קובץ אחר. התכנים שלו צמודים לתצורה שהוגדרה על ידי RFC 5280. ההגדרות מבוטאות ב־ASN.1 שהיא שפה שמשמשת להגדרת תצורות קבצים או מבני נתונים (שווי ערך). למשל, בשפת C ניתן לכתוב:

struct point {
  int x, y;
  char label[10];
};

ב־Go אפשר לכתוב:

type point struct {
  x, y int
  label string
}

וב־ASN.1 ניתן לכתוב:

Point ::= SEQUENCE {
  x INTEGER,
  y INTEGER,
  label UTF8String
}

היתרון של כתיבת הגדרות ASN.1 על פני הגדרות Go או C היא שזה לא תלוי שפה. ניתן להטמיע את הגדרת Point של ASN.1 בכל שפה או (אפילו עדיף) ניתן להשתמש בכלי שלוקח את הגדרת ה־ASN.1 ומייצר קוד אוטומטית ולהטמיע אותו בשפה המועדפת עליך. סדרה של הגדרות ASN.1 נקראת „מודול”.

הדבר החשוב הנוסף בנוגע ל־ASN.1 הוא שהוא מסופק עם מגוון תצורות/דרכי סריאליזציה להפוך מבנה נתונים מהזיכרון לסדרה של בתים (או קובץ) ולהפך. יכולת זו מאפשרת קריאה של אישור שנוצר במכונה אחת במכונה אחרת, אפילו אם אותה המכונה משתמשת במעבד ובמערכת הפעלה שונים.

ישנן כמה שפות אחרות שעושות את אותם הדברים כמו ASN.1. למשל, Protocol Buffers מציע גם שפה for להגדרת סוגים ותצורת סריאליזציה להצפנת פריטים מהסוגים שהגדרת. ל־Thrift גם יש שפה ותצורת סריאליזציה. ב־Protocol Buffers וגם ב־Thrift ניתן היה להשתמש להגדרת תצורת אישור ה־HTTPS, אך ל־ASN.1 ‏(1984) היה את היתרון המשמעותי שהוא כבר היה קיים כשאישורים (1988) ו־HTTPS‏ (1994) הגיחו לאוויר העולם.

ASN.1 עבר כמה גלגולים במהלך השנים, חלק מהמהדורות סומנו בהתאם לשנת הפרסום שלהן. מטרת המסמך הזה היא ללמד מספיק ASN.1 כדי להבין בבירור את RFC 5280 ותקנים אחרים שקשורים לאישורי HTTPS, לכן אנו נדון בעיקר על המהדורה של 1988, עם מספר הערות על יכולות שנוספו במהדורות עדכניות יותר. ניתן להוריד את המהדורות השונות ישירות מ־ITU, על אף שחלק מהן זמינות רק לחברי ITU. התקנים שקשורים לעניין הם X.680 (הגדרת שפת ASN.1) ו־X.690 (הגדרת תצורות הסריאליזציה DER ו־BER). מהדורות קודמות של התקנים האלו היו X.208 ו־X.209, בהתאמה.

תצורת הסריאליזציה העיקרית של ASN.1 היא „Distinguished Encoding Rules” (כללי הצפנה מובחנים - DER). מדובר בהגוון של „Basic Encoding Rules” (כללי הצפנה בסיסיים - BER) עם תוספת של יכולות הסבה קנונית. למשל, אם סוג כולל SET OF, יש לסדר את החברים לסריאליזציה של DER.

אישור שמיוצג ב־DER בדרך כלל מוצפן ל־PEM שמשתמש ב־base64 כדי להצפין בתים שרירותיים לכדי תווים אלפאנומריים (ו־‚+’ ו־‚/’) ומוסיף קווי הפרדה („-----BEGIN CERTIFICATE-----” ו־„-----END CERTIFICATE-----”). PEM הוא שימושי כיוון שקל יותר להעתיק ולהדביק אותו.

המסמך הזה יתאר תחילה את הסוגים ואת אופן הכתיבה שמשמש את ASN.1 ואז יתאר איך פריטים שמוגדרים בעזרת ASN.1 מוצפנים. אפשר לקפוץ באופן חופשי קדימה ואחורה בין הפסקאות, במיוחד כיוון שחלק מהתכונות בשפת ASN.1 מציינות באופן ישיר את פרטי ההצפנה. מסמך זה מעדיף מונחים מוכרים יותר ולכן משתמש ב„בית” במקום ב„אוקטטה” וב„ערך” במקום „תכנים”. הוא משתמש ב„סריאליזציה” וב„הצפנה” לסירוגין.

הסוגים

INTEGER

ה־INTEGER המוכר והטוב. יכול להיות חיובי או שלילי. מה שבאמת חריג ב־INTEGERs של ASN.1 הוא שהם יכולים להיות גדולים באופן שרירותי. אין מספיק מקום ב־int64? אין בעיה. מצב זה הוא שימושי מאוד במקרים של ייצוג דברים כמו שארית חלוקה ב־RSA שהיא גדולה משמעותית מ־int64 (גדול בכיוון של ‎22048). ברמה הטכנית יש מספר שלם מרבי ב־DER אבל הוא גדול באופן חריג: האורך של כל שדה ב־DER ניתן לביטוי כסדרה של עד 126 בתים. לכן ה־INTEGER הגדול ביותר שניתן לייצג ב־DER הוא ‎256(2**1008)-1. בשביל INTEGER שבאמת אין לו גבולות יש להצפין ב־BER שמאפשר שדות גדולים ללא סוף.

מחרוזות

ל־ASN.1 יש מגוון סוגי מחרוזות: BMPString,‏ GeneralString,‏ GraphicString,‏ IA5String,‏ ISO646String,‏ NumericString,‏ PrintableString,‏ TeletexString,‏ T61String,‏ UniversalString,‏ UTF8String,‏ VideotexString ו־VisibleString. למטרות אישורי HTTPS בעיקר מעניינים אותנו PrintableString,‏ UTF8String ו־IA5String. סוג המחרוזת לשדה מסוים מוגדרת על ידי מודול ASN.1 שמגדיר את השדה. למשל:

CPSuri ::= IA5String

PrintableString היא תת־סדרה מצומצמת של ASCII, שמאפשרת תווים אלפאנומריים, רווחים וסדרה מצומצמת של סימני פיסוק: ' () + , - . / : = ?. שווה לשים לב כי * או @ אינם חלק מהסדרה. אין יתרונות בגודל האחסון על פני סוגי מחרוזות מגבילים יותר.

חלק מהשדות, כגון DirectoryString ב־RFC 5280, מאפשרים לקוד הסריאליזציה לבחור מבין מגוון סוגי מחרוזות. מאחר שהצפנת DER את סוג המחרוזת בשימוש, יש לוודא שברגע שמשהו הוצפן ב־PrintableString הוא באמת עומד בדרישות של PrintableString.

IA5String, שמבוסס על האלפבית הבינלאומי מס׳. 5, הוא יותר מתירני: הוא מאפשר כמעט כל תו ASCII והוא משמש לכתובות דוא״ל, שמותDNS וכתובות באישורים. נא לשים לב שיש מעט ערכי בתים שמשמעות ה־IA5 של ערך הבית שונה מהמשמעות ב־US-ASCII של אותו הערך.

השימוש ב־TeletexString,‏ BMPString ו־UniversalString הופסק באישורי HTTPS אך אפשר עדיין להיתקל בהם בפענוח של אישורים של רשויות אישורים ותיקות יותר שכבר חיים זמן רב ועשויים להתעכב בהוצאתם מחוץ לשימושם.

מחרוזות ב־ASN.1 אינן מסתיימות ב־null כמו מחרוזות ב־C או ב־C++‎. למעשה, זה חוקי לגמרי שיש בתים עם null מפוזרים להם באמצע. זה יכול לגרום לחולשות כאשר שתי מערכות מפענחות את אותה מחרוזת ASN.1 בצורה שונה. למשל, את חלק מרשויות האישורים היה אפשר לרמות כדי להנפיק עבור „example.com\0.evil.com” בזכות הבעלות על evil.com. ספריות תיקוף אישורים באותה העת התייחסות לתוצאה כתקפה עבור „example.com”. יש לנקוט במשנה זהירות בעת טיפול במחרוזות ASN.1 ב־C וב־C++‎ כדי להימנע מיצירת חולשות.

תאריכים ושעות

יש מגוון רחב של סוגי זמן: UTCTime,‏ GeneralizedTime,‏ DATE,‏ TIME-OF-DAY,‏ DATE-TIME ו־DURATION. לאישורי HTTPS מעניין אותנו רק UTCTime ו־GeneralizedTime.

UTCTime מייצג את התאריך והשעה בצורה YYMMDDhhmm[ss], עם היסט אזור זמן כרשות „Z” לייצוג זולו (כלומר UTC כלומר אזור זמן בהיסט 0). למשל חותמות הזמן 820102120000Z ו־820102070000-0500 מסוג UTCTime מייצגות את אותה השעה: 2 בינואר, 1982, שעה 7 בבוקר בניו יורק (UTC-5) וב־12 בצהריים לפי UTC.

מאחר ש־UTCTime עשוי להתפרש במגוון צורות כגון במהלך המאה ה־20 (1900) או המאה ה־21 (2000), RFC 5280 מבהיר שהוא מייצג תאריכים מ־1950 עד 2050. RFC 5280 מחייב גם להשתמש באזור הזמן „Z” ולציין את השניות.

GeneralizedTime תומך בתאריכים אחרי 2050 בעזרת אמצעי פשוט של ייצוג השנה בארבע ספרות. הוא גם מאפשר שניות בשבר עשרוני (באופן חריג גם עם פסיק וגם עם נקודה בתור מפריד עשרוני). RFC 5280 אוסר על שניות כשבר עשרוני ודורש את ה־„Z”.

OBJECT IDENTIFIER

מזהי פריטים או OIDs הם יחודיים באופן גלובלי, מזהים היררכיים מורכבים מרצף של מספרים שלמים. הם יכולים להפנות לכל „דבר” אך הם בדרך כלל משמשים לזיהוי תקנים, אלגוריתמים, הרחבות של אישורים, ארגונים או מסמכי מדיניות. למשל: 1.2.840.113549 הוא הזיהוי של RSA Security LLC. כך RSA יכולים להקצות OIDs (מזהי פריטים) שמתחילים בקידומת הזאת, כגון 1.2.840.113549.1.1.11, שהוא הזיהוי של sha256WithRSAEncryption, כפי שמוגדר ב־RFC 8017.

באופן דומה, 1.3.6.1.4.1.11129 הוא המזהה של Google, Inc. ‏Google הקצתה את 1.3.6.1.4.1.11129.2.4.2 לזיהוי הרחבת רשימת SCT בה משתמשים בשקיפות אישורים (שפותחה במקור על ידי Google), כפי שמוגדר ב־RFC 6962.

סדרת מזהי הפריטים הצאצאים שיכולים להתקיים תחת קידומת מסוימת נקראת „OID arc”. מאחר שהייצוג של מזהי פריטים קצרים יותר הוא קטן יותר, הקצאות של מזהי פריטים תחת arcs קצרים יותר נחשב לבעל ערך גבוה יותר, במיוחד לתצורות שבהן יש לשלוח את מזהה הפריט פעמים רבות. ה־arc של מזהה הפריט 2.5 מוקצה ל„שירותי ספרייה”, סדרת המפרטים שכוללים את X.509, עליו מבוססים אישורי HTTPS. שדות רבים באישורים מתחילים ב־arc הקצר והנוח הזה. למשל, 2.5.4.6 זה „countryName”, בעוד 2.5.4.10 זה „organizationName”. מאחר שעל רוב האישורים להצפין את כל אחד ממזהי הפריט האלו לפחות פעם אחת, עדיף שהם יהיו קצרים.

מזהי פריטים (OIDs) במפרטים בדרך כלל מיוצגים בשם שאפשר לקרוא בקלות מטעמי נוחות והוא עשוי להיות מחובר למזהה פריט אחר. למשל מ־RFC 8017:

   pkcs-1    OBJECT IDENTIFIER ::= {
       iso(1) member-body(2) us(840) rsadsi(113549) pkcs(1) 1
   }
   ...

   sha256WithRSAEncryption      OBJECT IDENTIFIER ::= { pkcs-1 11 }

NULL

NULL זה פשוט NULL, יש מה להוסיף?

SEQUENCE ו־SEQUENCE OF

שהשמות לא יבלבלו אותך: אלו שני סוגים שונים לחלוטין. SEQUENCE הוא המקבילה של „struct” ברוב שפות התכנות. הוא מאכלס מספר קבוע של שדות מסוגים שונים. למשל, כמו באישור לדוגמה שלהלן.

SEQUENCE OF, מאידך, מאכלס מאכלס מספר שרירותי של שדות מסוג בודד. מזכיר מערך או רשימה בשפות התכנות השונות. למשל:

   RDNSequence ::= SEQUENCE OF RelativeDistinguishedName

זה יכול להיות 0, 1 או 7,000 RelativeDistinguishedNames, בסדר מסוים.

מסתבר ש־SEQUENCE ו־SEQUENCE OF חולקים דמיון כלשהו - שניהם מוצפנים באותו האופן! אפשר ללמוד על כך עוד בסעיף הצפנה.

SET ו־SET OF

אלו פחות או יותר אותו דבר כמו SEQUENCE ו־SEQUENCE OF למעט העובדה שאין סמנטיקה שמצורפת לסדר הפריטים שבהם. עם זאת בצורה המוצפנת הם חייבים להיות מסודרים. דוגמה:

RelativeDistinguishedName ::=
  SET SIZE (1..MAX) OF AttributeTypeAndValue

לתשומת לבך: דוגמה זו משתמשת במילת המפתח SIZE כדי לציין בנוסף שב־RelativeDistinguishedName חייב להיות חבר אחד לפחות, אך באופן כללי מותר שהגודל של SET או של SET OF יהיה אפס.

BIT STRING ו־OCTET STRING

אלו מכילים סיביות או בתים שרירותיים בהתאמה. אפשר להשתמש בהם לנתונים בלתי מובנים, כגון אסימונים מוצפנים או פלט של פונקציות גיבוב. אפשר גם להשתמש בהם כמו מצביע ריק (void) ב־C או בסוג מנשק ריק ב (interface{}‎) ב־Go: דרך להחזיק נתונים ללא מבנה אך במקום בו המבנה מובן או מוגדר בנפרד ממערכת הסוגים. למשל, חתימה על אישור מוגדרת בתור BIT STRING:

Certificate  ::=  SEQUENCE  {
     tbsCertificate       TBSCertificate,
     signatureAlgorithm   AlgorithmIdentifier,
     signature            BIT STRING  }

גרסאות עדכניות יותר של ASN.1 מאפשרות מפרט מפורט יותר של התכנים שבתוך ה־BIT STRING (כנ״ל לגבי OCTET STRINGs).

CHOICE ו־ANY

CHOICE הוא סוג שיכול להכיל בדיוק את אחד הסוגים שמופיעים בהגדרות שלו. למשל, הזמן יכול להכיל רק אחד מבין UTCTime או GeneralizedTime:

Time ::= CHOICE {
     utcTime        UTCTime,
     generalTime    GeneralizedTime }

ANY מציין שהערך יכול להיות מכל סוג. למעשה, הוא בדרך כלל מוגבל על ידי דברים שאי אפשר לבטא בצורה נוחה בתחביר של ASN.1. למשל:

   AttributeTypeAndValue ::= SEQUENCE {
     type     AttributeType,
     value    AttributeValue }

   AttributeType ::= OBJECT IDENTIFIER

   AttributeValue ::= ANY -- DEFINED BY AttributeType

שימושי במיוחד להרחבות, בהן רצוי להשאיר מקום לשדות נוספים כדי להגדיר אותם בנפרד לאחר פרסום המפרט העיקרי, לכן יש לך דרך לרשום סוגים חדשים (מזהי פריטים) ולאפשר להגדרות הסוגים האלו לציין מה אמור להיות המבנה של השדות החדשים.

נא לשים לב ש־ANY הוא שריד של אופן הכתיבה של ASN.1 מ־1988. במהדורת 1994 , השימוש ב־ANY הופסק והוחלף במחלקות פריטי מידע (Information Object Classes), שהם דרך מהודרת ומסודרת לציין את סוג התנהגות ההרחבה שאנשים ציפו לה מ־ANY. השינוי הזה כל כך ישן כיום שהמפרט העדכני ביותר של ASN.1 (מ־2015) לא מזכיר אפילו ANY. אבל כשמעיינים במהדורה מ־1994 ניתן לראות כמה דיונים על המעבר. התחביר הישן מצורף כאן כיוון שעדיין נעשה בזה שימוש ב־RFC 5280. RFC 5912 משתמש בתחביר של ASN.1 מ־2002 כדי לבטא את אותם הסוגים מ־RFC 5280 ועוד כמה מפרטים שקשורים אליהם.

צורת כתיבה אחרת

הערות נפתחות ב־--. שדות מסוג SEQUENCE או SET אפשר לסמן בתור OPTIONAL או שאפשר לסמן אותם גם בתור DEFAULT foo, שזה אותו הדבר כמו OPTIONAL רק שכאשר השדה חסר הוא ייחשב כאילו מופיע בו „foo”. לסוגים עם אורך (מחרוזות, אוקטט ומחרוזות סיביות, סדרות ורצפים של דברים) אפשר להעניק משתנה SIZE שמגביל את אורכם, או לאורך מסוים או לטווח מסוים.

אפשר להגביל סוגים כך שיהיו להם ערכים מסוימים באמצעות סוגריים מסולסלים אחרי הגדרת הסוג. הדוגמה הזאת מגדירה שהשדה Version יכול להכיל שלושה ערכים ומקצה שמות משמעותיים לערכים האלו:

Version ::= INTEGER { v1(0), v2(1), v3(2) }

זה בדרך כלל משמש בהקצאת שמות למזהי פריטים מסוימים (נא לשים לב שזה ערך בודד, ללא פסיקים שמציינים ערכים חלופיים). דוגמה מתוך RFC 5280.

id-pkix  OBJECT IDENTIFIER  ::=
         { iso(1) identified-organization(3) dod(6) internet(1)
                    security(5) mechanisms(5) pkix(7) }

זה יכול גם להיות [number], IMPLICIT,‏ EXPLICIT,‏ UNIVERSAL ו־APPLICATION. אלו מגדירים את הפרטים של איך להצפין ערכים, נרחיב על כך בהמשך.

ההצפנה

ASN.1 מקושר למגוון הצפנות: BER,‏ DER,‏ PER,‏ XER ועוד. כללי הצפנה בסיסיים (BER) הם די גמישים. כללי הצפנה מובחנים (DER) הם תת־סדרה של BER עם כללי קנוניקליזציה כדי שתהיה דרך אחת בלבד לבטא מבנה נתון. כללי הצפנה ארוזים (PER) משתמשים בפחות בתים כדי להצפין דברים, לכן הם שימושיים כשהמקום או זמן ההעברה צפופים. כללי הצפנה ב־XML ‏(XER) שימושיים אם מסיבה כלשהי החלטת שבא לך להשתמש ב־XML.

אישורי HTTPS בדרך כלל מוצפנים ב־DER. אפשר להצפין אותם ב־BER אך מאחר שערך החתימה מחושב על פני הצפנת DER המקבילה, לא בהכרח לפי אותם הבתים בדיוק כמו באישור, הצפנת אישור ב־BER מזמינה צרות לא הכרחיות. עוד יוסבר פה על BER תוך כדי ההסברים על המגבלות הנוספות ש־DER מציע.

המלצתנו היא לקרוא את הסעיף הזה תוך כדי פתיחה של פענוח של אישור אמתי בחלון אחר.

סוג-אורך-ערך

BER היא הצפנת סוג-אורך-ערך, בדיוק כמו Protocol Buffers ו־Thrift. המשמעות של כך היא שכשקוראים בתים שמוצפנים ב־BER, קודם כל נתקלים בסוג, שנקרא ב־ASN.1 תגית. זה בית או סדרה של בתים, שמציינים איזה סוג של דברים מוצפנים: INTEGER, או UTF8String או מבנה או כל דבר אחר.

סוג אורך ערך
02 03 ‎01 00 01‎

הבא שניתקל בו זה אורך: מספר שאומר לך כמה בתים של נתונים עליך לקרוא כדי לקבל את הערך. לאחר מכן, כמובן, מגיעים הבתים שמכילים את הערך עצמו. למשל, הבתים ההקסדצימליים ‎02 03 01 00 01 ייצגו INTEGER (תגית 02 תואמת לסוג INTEGER), עם אורך 03 וערך באורך שלושה בתים שמורכב מ־01 00 01.

סוג-אורך-ערך הוא נבדל מהצפנות עם הפרדה כמו JSON,‏ CSV או XML, בהם במקום לדעת את אורך השדה שאחרי, קוראים בתים עד שמגיעים למפריד הצפוי (למשל: { ב־JSON או ‎</some-tag>‎ ב־XML).

תגית

התגית היא בדרך כלל תו אחד. ישנן דרכים להצפין באופן שרירותי מספרי תגיות גדולים באמצעות מספר בתים (צורת „מספר תגית גבוה”), אך לרוב זה לא הכרחי.

הנה כמה תגיות לדוגמה:

תגית (עשרונית) תגית (הקסדצימלי) סוג
2 02 INTEGER
3 03 BIT STRING
4 04 OCTET STRING
5 05 NULL
6 06 OBJECT IDENTIFIER
12 0C UTF8String
16 ‪10 (וגם 30)* SEQUENCE ו־SEQUENCE OF
17 ‪11 (וגם 31)* SET ו־SET OF
19 13 PrintableString
22 16 IA5String
23 17 UTCTime
24 18 GeneralizedTime

אלו, וקומץ של נוספות שאינן מופיעות כאן כי הן משעממות, הן התגיות ה„אוניברסליות” כיוון שהן מפורטות במפרט של ASN.1 ויש להן את אותן המשמעות על פני כל המודולים של ASN.1.

כל התגיות האלו הן במקרה מתחת ל־31 (0x1F) ויש לכך סיבה טובה: סיביות 8, 7 ו־6 (הסיביות הגבוהות של בית התגית) משמשות להצפנת מידע נוסף, לכן כל מספר תגית אוניברסלית מעבר ל־31 יצטרך להשתמש בצורה „מספר תגית גבוהה”, שמשתמש בבתים נוספים. יש כמה תגיות אוניברסליות שימושיות מעל 31 אך הן די נדירות.

שתי התגיות שמסומנות ב־* תמיד מוצפנות בתור 0x30 או 0x31, כיוון שסיבית מס׳ 6 משמשת לציון האם שדה הוא מבני או פרימיטיבי. התגיות האלו הן תמיד מבניות ולכן בהצפנה שלהן סיבית מס׳ 6 מוגדרת ל־1. בסעיף מבני מול פרימיטיבי יש פרטים נוספים.

מחלקות תגיות

רק כיוון שהמחלקה האוניברסלית השתמשה בכל מספרי התגיות ה„טובים” אין זה אומר שאין לנו מזל בכל הנוגע להגדרת תגיות משלנו. יש גם את המחלקות „application”,‏ „private” ו־„context-specific”. אלו נבדלות על ידי הסיביות 8 ו־7:

מחלקה סיבית 8 סיבית 7
Universal (אוניברסלית) 0 0
Application (יישום) 0 1
Context-specific (תלוית הקשר) 1 0
Private (פרטית) 1 1

מפרטים בדרך כלל משתמשים בתגיות במחלקה האוניברסלית כיוון שהן מספקות את אבני הבניין המשמעותיות ביותר. למשל, המספר הסידורי באישור מוצפן בעזרת INTEGER הישן והטוב, תגית מספר 0x02. אבל לפעמים מפרט צריך להגדיר תגיות במחלקה תלוית ההקשר כדי לבדל בין רשומות ב־SET או ב־SEQUENCE שמגדירות רשומות רשות או ליצור הבדל ברור ב־CHOICE עם מגוון רשומות מאותו הסוג. למשל, אפשר לעיין בהגדרה הבאה:

Point ::= SEQUENCE {
  x INTEGER OPTIONAL,
  y INTEGER OPTIONAL
}

מאחר ששדות ה־OPTIONAL מושמטים לחלוטין מההצפנה כשאינם נמצאים, זה בלתי אפשרי להבדיל בין נקודה עם ערך רק בציר x לבין נקודה עם ערך רק בציר y. למשל, נקודה שיש לה את הערך 9 רק בציר ה־x מצפינים באופן הבא (30 כאן מציין SEQUENCE):

‎30 03 02 01 09

זה SEQUENCE באורך 3 (בתים), שמכיל INTEGER באורך 1, עם הערך 9. אבל נקודה עם הערך 9 בציר y תוצפן בדיוק באותו האופן, לכן ישנה אי־ודאות מסוימות.

הנחיות הצפנה

כדי לפתור את אי־הוודאות הזאת, מפרט צריך לספק הנחיות הצפנה שמקצות תגית ייחודית לכל רשומה. וכיוון שאסור לדרוך על תגיות מסוג UNIVERSAL, יש להשתמש באחד האחרים, למשל ב־APPLICATION:

Point ::= SEQUENCE {
  x [APPLICATION 0] INTEGER OPTIONAL,
  y [APPLICATION 1] INTEGER OPTIONAL
}

למרות שבמקרה הזה יותר נפוץ להשתמש במחלקה תלוית הקשר שמיוצגת על ידי מספר בסוגריים באופן עצמאי:

Point ::= SEQUENCE {
  x [0] INTEGER OPTIONAL,
  y [1] INTEGER OPTIONAL
}

אז עכשיו, כדי להצפין נקודה עם נקודה שיש לה רק ערך 9 בציר y, במקום להצפין את x בתור UNIVERSAL INTEGER, מגדירים את סיבית 8 ו־7 של התגית המוצפנת ל (1, 0) כדי לציין את מחלקת ההקשר המסוימת ולהגדיר את הסיביות התחתונות ל־0, מה שנותן את ההצפנה הבאה:

‎30 03 80 01 09

וכדי לייצג נקודה עם נקודה עם ערך 9 בציר y, אפשר לעשות את אותו הדבר רק שמגדירים את הסיביות התחתונות ל־1:

‎30 03 81 01 09

או שאפשר לייצג נקודה שבה הערכים ב־x וב־y שניהם 9:

‎30 06 80 01 09 81 01 09

אורך

האורך בצירוף תגית-אורך-ערך תמיד מייצג את המספר הכולל של הבתים בפריט כולל כל תת־הפריטים. לכן ל־SEQUENCE עם שדה אחד לא יהיה את האורך 1; האורך שלו יהיה מספר הבתים של הצורה המוצפנת שהשדה תופס.

ההצפנה של האורך יכולה לקבל שתי צורות: קצרה וארוכה. הצורה הקצרה היא בית בודד בין 0 ל־127.

הצורה הארוכה היא באורך של שני בתים לפחות וסיבית 8 של הבית הראשון מוגדרת ל־1. סיביות 7-1 של הבית הראשון מציינות כמה עוד בתים הם באורך השדה עצמו. אז הבתים שנותרו מציינים את האורך עצמו כמספר שלם מרובה בתים.

כפי שניתן לתאר, מצב כזה מאפשר ערכים ארוכים במיוחד. האורך המרבי האפשרי יתחיל בבית 254 (בית אורך על סך 255 שמור להרחבות עתידיות), מציין 126 בתים עוקבים נוספים לשדה האורך לבדו. אם כל אחד מבין 126 הבתים האלה היה 255, זה אומר שלשדה הערך יש 21008-1 בתים עוקבים.

הצורה ארוכה מאפשרת לך להצפין את אותו האורך במגוון דרכים - למשל על ידי שימוש בשני בתים לביטוי אורך שיכול להיכנס באחד או להשתמש בצורה הארוכה כדי לציין אורך שיכול להיכנס בצורה הקצרה. המפרט של DER מציין שתמיד יש להשתמש בייצוג האורך הקצר ביותר.

אזהרת בטיחות: לא לסמוך לחלוטין על ערכי האורך שמפוענחים! למשל, יש לבדוק שהאורך המוצפן הוא קטן מכמות הנתונים הזמינה בתזרים שמפוענח.

אורך אינסופי

ב־BER ניתן בנוסף להצפין מחרוזת, SEQUENCE,‏ SEQUENCE OF,‏ SET או SET OF כשהאורך לא ידוע מראש (למשל בעת הזרמת פלט). כדי לעשות זאת, יש להצפין את האורך כבית בודד עם הערך 80 ולהצפין את הערך כסדרה של פריטים מוצפנים מחוברים יחדיו, כשהסוף מצוין באמצעות שני הבתים 00 00 (שניתן להתייחס אליהם כאל פריט באורך אפס עם התגית 0). לכן, למשל, הצפנת אורך אינסופי של UTF8String יהיה הצפנה של UTF8String אחד או יותר, מחוברים יחדיו, שבסופם מצורפים 00 00.

אפשר לקנן אינסופיות באופן שרירותי! לכן, למשל, את ה־UTF8Strings שמצורפות יחדיו לכדי UTF8String באורך אינסופי ניתן להצפין בעצמן עם אורך סופי או אינסופי.

בית אורך עם הערך 80 הוא מבדיל כיוון שזה אורך לא תקני בצורה קצרה או בצורה ארוכה. מאחר שסיבית 8 מוגדרת ל־1, זה בדרך כלל יפוענח בתור הצורה הארוכה, אך הסיביות הנותרות אמורות לציין את מספר הבתים הנוספים שמרכיבים את האורך. מאחר שסיביות 7-1 כולן 0, מצב כזה עשוי לציין הצפנה בצורה ארוכה עם אפס בתים שמרכיבים את האורך, שזה אסור.

DER אוסר על הצפנה באורך אינסופי. עליך להשתמש בהצפנת אורך סופי (כלומר שהאורך מצוין בהתחלה).

מבני מול פרימיטיבי

סיבית 6 של בית התגית הראשון משמש לציון האם הערך מוצפן בצורה פרימיטיבית או מובנית. הצפנה פרימיטיבית מייצגת את הערך ישירות - למשל, ב־UTF8String הערך יורכב מהמחרוזת עצמה בלבד, בבתים מסוג UTF-8. הצפנה מבנית מייצגת את הערך כצירוף של ערכים מוצפנים אחרים. למשל, כפי שמתואר בסעיף „אורך אינסופי”, UTF8String בהצפנה מבנית תורכב ממספר UTF8Strings מוצפנות (כל אחת עם תגית ואורך), מצורפות יחדיו. אורך ה־UTF8String הכולל יהיה האורך הכולל, בבתים, של כל הערכים המוצפנים האלה כשהם מצורפים יחד. הצפנה מבנית יכולה להשתמש באורך סופי או אינסופי. הצפנה פרימיטיבית תמיד משתמשת באורך סופי, כיוון שאין דרך לבטא אורך אינסופי מבלי להשתמש בהצפנה מבנית.

ב־INTEGER,‏ OBJECT IDENTIFIER ו־NULL חובה להשתמש בהצפנה פרימיטיבית. ב־SEQUENCE,‏ SEQUENCE OF,‏ SET ו־SET OF חובה להשתמש בהצפנה מבנית (כיוון ביסוד מדובר בצירוף של מספר ערכים). BIT STRING,‏ OCTET STRING,‏ UTCTime,‏ GeneralizedTime ומגוון סוגי המחרוזות יכולים להשתמש בהצפנה פרימיטיבית או מובנית, ללא ידיעת המשתמש -- ב־BER. עם זאת, ב־DER, כל הסוגים שיש לגביהם בחירה בהצפנה בין פרימיטיבית למבנית חובה להשתמש בהצפנה הפרימיטיבית.

EXPLICIT מול IMPLICIT

הנחיות ההצפנה שמתוארות להלן, למשל: [1] או [APPLICATION 8], יכולות לכלול את מילות המפתח EXPLICIT או IMPLICIT (דוגמה מתוך RFC 5280):

TBSCertificate  ::=  SEQUENCE  {
     version         [0]  Version DEFAULT v1,
     serialNumber         CertificateSerialNumber,
     signature            AlgorithmIdentifier,
     issuer               Name,
     validity             Validity,
     subject              Name,
     subjectPublicKeyInfo SubjectPublicKeyInfo,
     issuerUniqueID  [1]  IMPLICIT UniqueIdentifier OPTIONAL,
                          -- If present, version MUST be v2 or v3
     subjectUniqueID [2]  IMPLICIT UniqueIdentifier OPTIONAL,
                          -- If present, version MUST be v2 or v3
     extensions      [3]  Extensions OPTIONAL
                          -- If present, version MUST be v3 --  }

מגדיר כיצד להצפין את התגית, אין לו קשר להאם מספר התגית מוקצה מפורשות או לא (מאחר שגם IMPLICIT וגם EXPLICIT תמיד מצורפים למספר תגית מסוים). IMPLICIT מצפינה את השדה בדיוק כמו הסוג שמתחת אך עם מספר תגית ומחלקה שמסופקים במודול ASN.1. EXPLICIT מצפינה את השדה כמו הסוג שמתחת ואז עוטפת את זה בהצפנה חיצונית. להצפנה החיצונית יש את מספר התגית והמחלקה ממודול ASN.1 ובנוסף מוגדרת לה סיבית מבנית.

הנה דוגמה של הנחיות הצפנה של ASN.1 באמצעות IMPLICIT:

[5] IMPLICIT UTF8String

יצפין את „hi” בתור:

‎85 02 68 69

בהשוואה להנחיות הצפנה אלו ב־ASN.1 שמשתמשות ב־EXPLICIT:

[5] EXPLICIT UTF8String

יצפין את „hi” בתור:

‎A5 04 0C 02 68 69

כאשר מילות המפתח IMPLICIT או EXPLICIT אינן מופיעות, בררת המחדל היא EXPLICIT, אלא אם כן המודול מגדיר בררת מחדל אחרת בפתיחה באמצעות „EXPLICIT TAGS”,‏ „IMPLICIT TAGS” או „AUTOMATIC TAGS”. למשל, RFC 5280 מגדיר שני מודולים, האחד בו תגיות EXPLICIT הן בררת המחדל, והשני שמייבא את הראשון ואצלו תגיות IMPLICIT הן בררת המחדל. הצפנה מרומזת (Implicit) משתמשת בפחות בתים מהצפנה מפורשת (Explicit).

AUTOMATIC TAGS זהה ל־IMPLICIT TAGS, אך עם מאפיין נוסף בו מספרי תגיות ([0],‏ [1] וכו׳) מוקצים אוטומטית במקומות בהם זה נדרש, כמו SEQUENCEs עם שדות נוספים.

הצפנה של סוגים מסוימים

בסעיף הזה נדון על האופן בו ערך של כל סוג מוצפן, עם דוגמאות.

הצפנת INTEGER

מספרים שלמים מוצפנים כבית אחד או יותר בשיטת המשלים לשתיים עם הסיבית הגבוהה (סיבית 8) של הבית השמאלי ביותר כסיבית חיובי או שלילי. כפי שכתוב במפרט של BER:

הערך של המספר המשלים לשתיים בבינרי נגזר ממספור הסיביות באוקטטות התוכן, החל מסיבית 1 באוקטט האחרון כסיבית אפס ועד לסוף המספור עם סיבית 8 של האוקטט הראשון. לכל סיבית מוקצה ערך מספרי של ‎2N, כאשר N זה המיקום שלה ברצף המספור שלעיל. הערך של המספר הבינרי בשיטת המשלים לשתיים מתקבל על ידי סיכום הערכים המספריים שמוקצים לכל סיבית עבור הסיביות שמוגדרות לאחד, למעט סיבית 8 של האוקטט הראשון ואז מקטנים את הערך הזה בערך המספרי שמוקצה לסיבית 8 של האוקטט הראשון אם הסיבית הזאת מוגדרת לאחד.

לכן, למשל, ערך זה באורך בית אחד (שמיוצג בבינרי) מצפין 50 בעשרוני:

00110010 (== ‎50 עשרוני)

ערך זה באורך בית אחד (שמיוצג בבינרי) מצפין ‎-100 עשרוני:

10011100 (== ‎-100 עשרוני)

ערך זה באורך חמישה בתים (שמיוצג בבינרי) מצפין את ‎-549755813887 (כלומר ‎-239 + 1) בעשרוני:

‎10000000 00000000 00000000 00000000 00000001 (== ‎-549755813887 עשרוני)

BER ו־DER שניהם דורשים שמספרים שלמים ייוצגו בצורה הקצרה ביותר שניתן. שנאכף עם הכלל הזה:

... הסיביות של האוקטט הראשון וסיבית 8 של האוקטט השני:

1.  לא תהיינה כולן אחדים, וגם
2.  לא תהיינה כולן אפסים.

כלל (2) אומר באופן כללי: אם יש בתים של אפס בהתחלה בהצפנה אפשר להשאיר אותם כבויים ולקבל את אותו המספר. סיבית 8 של הבית השני חשובה גם כאן כיוון שכדאי לייצג ערכים מסוימים, עליך להשתמש בבית אפס מקדים. למשל, 255 עשרוני מוצפן כשני בתים:

‎00000000 11111111

זה כיוון שהצפנה בבית אחד של 11111111 בעצמו נותן ‎-1 (סיבית 8 נחשבת כאן כסיבית סימן).

כלל (1) אפשר להסביר בצורה הפשוטה ביותר עם דוגמה. ‎-128 עשרוני מוצפן בתור:

10000000 (== ‎-128 עשרוני)

עם זאת, אפשר להצפין את זה גם בתור:

11111111 10000000 (== ‎-128 עשרוני, אבל בהצפנה שגויה)

אם מרחיבים את זה, מקבלים ‎-215 + 214 + 213 + 212 + 211 + 210 + 29 + 28 + 27 == -27 == ‎-128. נא לשים לב שה־1 ב־„10000000” היה סיבית סימן בהצפנה בבית אחד, אך בהצפנה בשני בתים המשמעות שלו היא 27.

זאת המרה כללית: לכל מספר שמוצפן כ־BER (או DER) אפשר לצרף בהתחלה 11111111 ולקבל את אותו המספר. זה נקרא הרחבת סימן. או, על אותו משקל, אם יש מספר שלילי בו ההצפנה של הערך מתחילה ב־11111111, אפשר להסיר את הבית הזה והמספר יישאר אותו דבר. לכן BER ו־DER דורשים את ההצפנה הקצרה ביותר.

להצפנת שיטת המשלים לשתיים של מספרים שלמים (INTEGERs) יש השפעה מעשית על הנפקת האישור: RFC 5280 דורש שהמספרים הסידוריים יהיו חיוביים. מאחר שהסיבית הראשונה היא תמיד סיבית סימן, משמעות הדבר היא שהמספר הסידורי מוצפן ב־DER כ־8 בתים יכול להיות באורך של 63 סיביות לכל היותר. הצפנת מספר סידורי באורך 64 סיביות דורש ערך בהצפנה של 9 בתים (כשהבית הראשון הוא אפס).

הנה ההצפנה של INTEGER עם הערך ‎263+1 (שהוא במקרה מספר חיובי באורך 64 סיביות):

‎02 09 00 80 00 00 00 00 00 00 01

הצפנת מחרוזות

מחרוזות מוצפנות בתור הבתים המילוליים שלהן. מאחר ש־IA5String ו־PrintableString פשוט מגדירים תת־סדרות שונות של תווים מורשים, ההצפנות שלהן משתנות רק ברמת תגית.

PrintableString שמכילה „hi”:

‎13 02 68 69

IA5String שמכילה „hi”:

‎16 02 68 69

UTF8Strings הם זהים אך ניתן להצפין מגוון רחב יותר של תווים. למשל, זה הייצוג של UTF8String שמכיל את התו U+1F60E פרצוף מחייך עם משקפי שמש (😎):

‎0c 04 f0 9f 98 8e

הצפנת תאריך ושעה

UTCTime ו־GeneralizedTime מוצפנות למעשה כמו מחרוזות, באופן מפתיע! כפי שמתואר לעיל בסעיף „סוגים”, UTCTime מייצג תאריכים בצורה YYMMDDhhmmss. GeneralizedTime משתמש בשנה בארבע ספרות YYYY במקום YY. לשניהם יש היסט אזור זמן כרשות או „Z” ‏(Zulu) כדי לציין שאין הפרש מ־UTC.

למשל, 15 בדצמבר, 2019 בשעה 19:02:10 באזור הזמן PST‏ (UTC-8) מיוצג ב־UTCTime בתור: 191215190210-0800. בהצפנה ל־BER, זה יוצא:

‎17 11 31 39 31 32 31 35 31 39 30 32 31 30 2d 30 38 30 30

להצפנת BER שניות הן בגדר רשות גם ב־UTCTime וגם ב־GeneralizedTime ומותר הפרשי אזורי זמן. עם זאת, DER (יחד עם RFC 5280) מציינים שהשניות חייבות להופיע, אסור שתהיינה שניות עם שברים ואת השעה יש לציין כ־UTC עם הצורה „Z”.

התאריך הבא יוצפן ב־DER בתור:

‎17 0d 31 39 31 32 31 36 30 33 30 32 31 30 5a

הצפנת OBJECT IDENTIFIER

כפי שמתואר להלן, מזהי פריטים הם באופן עקרוני סדרה של מספרים שלמים. הם תמיד באורך של שני רכיבים לפחות. הרכיב הראשון הוא תמיד 0, 1 או 2. כאשר הרכיב הראשון הוא 0 או 1, הרכיב השני הוא תמיד קטן מ־40. לכן, שני הרכיבים הראשונים מיוצגים באופן שלא משתמע לשתי פנים בתור ‎40*X+Y כאשר X הוא הרכיב הראשון ו־Y הוא הרכיב השני.

אם כן, למשל, כדי להצפין את 2.999.3, יש לחבר את שני הרכיבים הראשונים ל־1079 בבסיס עשרוני (‎40*2 + 999) שנותן את הערך „1079.3”.

לאחר החלת ההמרה הזאת, כל רכיב מוצפן בבסיס 128, עם הבית המשמעותי ביותר תחילה. סיבית 8 מוגדרת ל־„1” בכל בית למעט באחון ברכיב, ככה ניתן לדעת מתי רכיב אחד מסתיים והבא אחריו מתחיל. לכן הרכיב „3” ייוצג בפשטות בתור הבית 0x03. הרכיב „129” ייוצג בתור הבתים 0x81 0x01. לאחר ההצפנה, כל הרכיבים של מזהה הפריט מחוברים יחד לכדי הערך המוצפן של מזהה הפריט.

מזהי פריטים יש לייצג בכמה שפחות בתים שניתן, בין אם ב־BER ובין אם ב־DER. לכן, רכיבים לא יכולים להיפתח בבית 0x80.

לדוגמה, מזהה הפריט 1.2.840.113549.1.1.11 (שמייצג את sha256WithRSAEncryption) מוצפן כך:

‎06 09 2a 86 48 86 f7 0d 01 01 0b

הצפנת NULL

ערך של פריט שמכיל NULL הוא תמיד באורך אפס, לכן ההצפנה של NULL היא תמיד רק התגית ושדה אורך על סך אפס:

‎05 00

הצפנת SEQUENCE

הדבר הראשון שכדאי לדעת על SEQUENCE (רצף) הוא שהוא תמיד משתמש בהצפנה מבנית כיוון שהוא מכיל פריטים אחרים. במילים אחרות, הבתים של הערך של SEQUENCE מכילים את צירוף השדות המוצפנים של אותו SEQUENCE (בסדר שבו השדות האלו הוגדרו). עובדה זו משליכה שסיבית 6 של תגית SEQUENCE (הסיבית מבני מול פרימיטיבי) תמיד מוגדרת ל־1. כך שגם אם מספר התגית ל־SEQUENCE הוא טכנית 0x10, בית התגית שלו, לאחר ההצפנה, הוא תמיד 0x30.

כאשר יש שדות ב־SEQUENCE עם הסימון OPTIONAL הם מושמטים מההצפנה אם אינם קיימים. בזמן שהמפענח מעבד את הרכיבים של ה־SEQUENCE הוא יכול להבין איזה סוג של נתונים מפוענח בהתאם למה שפוענח עד כה והבתים של התגיות שהוא קרא. במקרה של אי ודאות, למשל כאשר מספר רכיבים הם מאותו הסוג, מודול ה־ASN.1 חייב לציין הנחיות הצפנה שמקצות מספרי תגיות יחודיים לרכיבים.

שדות DEFAULT זהים לכאלו שהם OPTIONAL. אם ערך של שדה הוא בררת המחדל, אפשר להסיר אותו מהצפנת ה־BER. בהצפנת DER חובה להשמיט אותו.

לדוגמה, RFC 5280 מגדיר את AlgorithmIdentifier בתור SEQUENCE:

   AlgorithmIdentifier  ::=  SEQUENCE  {
        algorithm               OBJECT IDENTIFIER,
        parameters              ANY DEFINED BY algorithm OPTIONAL  }

הנה ההצפנה של ה־AlgorithmIdentifier שמכיל 1.2.840.113549.1.1.11. ב־RFC 8017 נכתב ש„משתנים” (parameters) אמורים להיות מסוג NULL עבור האלגוריתם הזה.

‎30 0d 06 09 2a 86 48 86 f7 0d 01 01 0b 05 00

הצפנת SEQUENCE OF

SEQUENCE OF מוצפן בדיוק באותו האופן כמו SEQUENCE. הוא אפילו משתמש באותה התגית! אם מטרתך היא פענוח, הדרך היחידה בה אפשר להבדיל בין SEQUENCE לבין SEQUENCE OF היא לפי ההפניה למודול ASN.1.

הנה ההצפנה של SEQUENCE OF INTEGER שמכיל את המספרים 7, 8 ו־9:

30 09 02 01 07 02 01 08 02 01 09

הצפנת SET

בדומה ל־SEQUENCE,‏ SET הוא מבני, כלומר שבתי הערך שלו הם חיבור של השדות המוצפנים שלו. מספר התגית שלו הוא 0x11. מאחר שהסיבית מבני מול פרימיטיבי (סיבית 6) תמיד מקובעת ל־1, משמעות הדבר היא שהיא מוצפנת עם בית תגית של 0x31.

ההצפנה של SET, בדומה ל־SEQUENCE משמיטה את השדות OPTIONAL ו־DEFAULT אם הם חסרים או שיש להם את ערכי בררת המחדל. כל דו־משמעות שעלולה לצוץ עקב שדות מאותו הסוג חייבת להיפתר על ידי מודול ASN.1 וחובה להשמיט את שדות ה־DEFAULT מהצפנת ה־DER אם הערכים שלהם הם בררת המחדל.

ב־BER אפשר להצפין את SET בכל סדר שהוא. ב־DER, יש להצפין את SET בסדר עולה לפי הערך הסידורי של כל רכיב.

הצפנת SET OF

פריטי SET OF מוצפנים באותו האופן כמו SET כולל בית התגית 0x31. להצפנת DER יש דרישה דומה ש־SET OF חייב להיות מוצפן בסדר עולה. כיוון שכל הרכיבים ב־SET OF הם מאותו הסוג, סידור לפי תגית אינו מספיק. לכן הרכיבים של SET OF מסודרים לפי הערכים המוצפנים שלהם, עם ערכים קצרים יותר שמתייחסים אליהם כאילו הם מרופדים מימין באפסים.

הצפנת BIT STRING

BIT STRING באורך N סיביות מוצפנת בתור N/8 בתים (בעיגול מעלה), עם קידומת של בית אחד שמכיל את „מספר הסיביות שאינן בשימוש”, לשם הבהרה כאשר מספר הסיביות אינו כפולה של 8. למשל, כאשר מצפינים את מחרוזת הסיביות 011011100101110111 (18 סיביות), אנו זקוקים לשלושה בתים לפחות. אבל זה קצת יותר ממה שאנחנו צריכים: הקיבולת שלהן היא 24 סיביות בסך הכול. שש מתוכן לא תהיינה בשימוש. שש הסיביות האלו ייכתבו בסוף הימני של מחרוזת הסיביות, לכן זה יוצפן באופן הבא:

‎03 04 06 6e 5d c0

ב־BER סיביות שאינן בשימוש יכולות שיהיה להן כל ערך שהוא, לכן הבית האחרון של ההצפנה הזאת יכול להיות כל אחד מבין c1,‏ c2,‏ c3 וכן הלאה. ב־DER, כל הסיביות שלא היו בשימוש חייבות להיות אפס.

הצפנת OCTET STRING

OCTET STRING מוצפנת בהתאם לבתים שהיא מכילה. הנה דוגמה ל־OCTET STRING שמכיל את הבתים 03, 02, 06 ו־A0:

‎04 04 03 02 06 A0

הצפנת CHOICE ו־ANY

שדה CHOICE או ANY מוצפן בתור איזשהו סוג שהוא מאכלס, למעט אם זה השתנה על ידי הנחיות ההצפנה. לכן, אם שדה CHOICE במפרט ASN.1 מאפשר INTEGER או UTCTime והפריט המסוים שמוצפן מכיל INTEGER, הוא יוצפן כ־INTEGER.

למעשה, לשדות CHOICE בדרך כלל יש הנחיות הצפנה. למשל, נביט בדוגמה הבאה מתוך RFC 5280, כאשר הנחיות ההצפנה נחוצות כדי להבדיל בין rfc822Name לבין dNSName, מאחר שהסוג שנמצא מתחת לשתיהן הוא IA5String:

   GeneralName ::= CHOICE {
        otherName                       [0]     OtherName,
        rfc822Name                      [1]     IA5String,
        dNSName                         [2]     IA5String,
        x400Address                     [3]     ORAddress,
        directoryName                   [4]     Name,
        ediPartyName                    [5]     EDIPartyName,
        uniformResourceIdentifier       [6]     IA5String,
        iPAddress                       [7]     OCTET STRING,
        registeredID                    [8]     OBJECT IDENTIFIER }

הנה הצפנה לדוגמה של GeneralName שמכיל a@exmaple.com מסוג rfc822Name (תוך שאנו זוכרים ש־‎[1] אומר להשתמש במספר תגית 1, במחלקת התגיות תלויות ההקשר (סיבית 8 מוגדרת ל־1), עם שיטת ההצפנה עם תגית IMPLICIT):

‎81 0d 61 40 65 78 61 6d 70 6c 65 2e 63 6f 6d

הנה דוגמה של הצפנת GeneralName שמכיל את ה־dNSName‏ „example.com”:

‎82 0b 65 78 61 6d 70 6c 65 2e 63 6f 6d

בטיחות

חשוב לנקוט במשנה זהירות בפענוח BER ו־DER, במיוחד בשפות שאינן בטוחות לזיכרון כמו C ו־C++‎. יש היסטוריה ארוכה של חולשות במפענחים. פענוח קלט באופן כללי הוא מקור נפוץ לחולשות. תצורות ההצפנה של ASN.1 הן מגנט שמושך אליו מגוון חולשות. הן תצורות מסובכות עם מגוון שדות באורכים משתנים. אפילו לאורכים יש אורכים משתנים! כמו כן, קלט ASN.1 נשלט בדרך כלל על ידי התוקף. אם עליך לפענח אישור כדי להבדיל בין משתמשים מורשים למשתמשים בלתי מורשים, עליך להניח שחלק מהזמן יהיה עליך לפענח משהו שאינו אישור אלא איזשהו קלט הזוי שתוכנן למצוא חולשות בקוד ה־ASN.1 שלך.

כדי להימנע מהבעיות האלו, תמיד עדיף להשתמש בשפות שמתנהלות מול הזיכרון בצורה בטוחה ככל שניתן. ובין אם יש לך אפשרות להשתמש בשפה שמתנהלת מול הזיכרון בצורה בטוחה או שלא, עדיף להשתמש במהדר ASN.1 כדי לייצר את קוד הפענוח שלך במקום לכתוב אותו מאפס.

הוקרת תודה

תודתי העמוקה נתונה למדריך של ליימן לתת־סדרות של ASN.1,‏ DER ו־BER, שהוא חלק משמעותי מהדרך בה למדתי את הנושאים האלו. נרצה גם להודות ליוצרים של קבלת פנים חמה ל־DNS, שהוא חומר נהדר לקריאה והשפיעה רבות על הלך הרוח של המסמך הזה.

בונוס קטן

יצא לך לשים לב שאישור בקידוד PEM תמיד מתחיל ב־„MII”? לדוגמה:

-----BEGIN CERTIFICATE-----

MIIFajCCBFKgAwIBAgISA6HJW9qjaoJoMn8iU8vTuiQ2MA0GCSqGSIb3DQEBCwUA
...

כעת יש לך מספיק ידע כדי להסביר מדוע! אישור הוא SEQUENCE, ולכן ייפתח בבית 0x30. הבתים הבאים הם שדה האורך. אישורים הם כמעט תמיד ארוכים מ־127 בתים, לכן על שדה האורך להשתמש בצורה הארוכה של האורך. משמעות הדבר היא שהבית הראשון יהיה 0x80 + N, כאשר N הוא מספר של אורך עוקב בבתים. N הוא כמעט תמיד 2, מאחר שזאת כמות התווים שנדרשת להצפין אורכים מ־128 ועד 65535 וכמעט לכל האישורים יש אורכים בטווח הזה.

אז עכשיו אנחנו יודעים ששני הבתים הראשונים של הצפנת DER של אישור הם ‎0x30 0x82‎. הצפנת PEM  משתמשת ב־base64, שמצפין 3 בתים של קלט בינרי לפלט של 4 תווי ASCII. או, במילים אחרות: base64 הופך קלט בינרי של 24 סיביות לפלט של 4 תווי ASCII עם 6 סיביות מהקלט שמוקצות לכל תו. אנחנו יודעים מה תהיינה 16 הסיביות הראשונות של כל אישור. כדי להוכיח שהתווים הראשונים של (כמעט) כל אישור יהיו „MII”, אנחנו צריכים להתבונן ב־2 הסיביות הבאות. הן תהיינה הסיביות המובהקות ביותר של הבית המובהק ביותר מתוך הבתים באורך שתיים. האם הסיביות האלו אי פעם יוגדרו להיות 1? לא, אלמלא אורך האישור הוא מעל 16,384 בתים! לכן אנחנו יכולים לחזות שהתווים הראשונים של אישור PEM תמיד יהיו אותו הדבר. אפשר לנסות לבד:

xxd -r -p <<<308200 | base64