// SPDX-FileCopyrightText: © 2022—2026 Vladimir Zorin // SPDX-License-Identifier: LicenseRef-OWL-1.0-or-later // Licensed under OWL v1.0+. See LICENSE. /* * LITLS -- Lilush TLS Stack * Minimal ASN.1/DER reader and writer. */ #include "asn1.h" #include /* ── DER reader ───────────────────────────────────────── */ void litls_der_init(litls_der_cursor *c, const uint8_t *buf, size_t len) { c->buf = buf; c->len = len; c->pos = 0; } /* Parse a DER length field starting at c->pos. Advances pos. * Returns the length or (size_t)-1 on error. */ static size_t der_read_length(litls_der_cursor *c) { if (c->pos >= c->len) return (size_t)-1; uint8_t first = c->buf[c->pos++]; if (first < 0x80) return first; if (first == 0x80) return (size_t)-1; /* indefinite length not allowed in DER */ int n = first & 0x7F; if (n > 4 || c->pos + n > c->len) return (size_t)-1; size_t length = 0; for (int i = 0; i < n; i++) length = (length << 8) | c->buf[c->pos++]; return length; } int litls_der_next(litls_der_cursor *c, litls_der_element *elem) { if (c->pos >= c->len) return -1; size_t start = c->pos; elem->tag = c->buf[c->pos++]; size_t length = der_read_length(c); if (length == (size_t)-1 || c->pos + length > c->len) return -1; elem->value = c->buf + c->pos; elem->length = length; c->pos += length; elem->total = c->pos - start; return 0; } int litls_der_enter(litls_der_cursor *sub, const litls_der_element *elem) { sub->buf = elem->value; sub->len = elem->length; sub->pos = 0; return 0; } int litls_der_skip(litls_der_cursor *c) { litls_der_element elem; return litls_der_next(c, &elem); } int litls_der_peek_tag(const litls_der_cursor *c, uint8_t *tag) { if (c->pos >= c->len) return -1; *tag = c->buf[c->pos]; return 0; } size_t litls_der_remaining(const litls_der_cursor *c) { return c->len - c->pos; } int litls_der_oid_eq(const litls_der_element *elem, const uint8_t *oid, size_t oid_len) { if (elem->tag != ASN1_OID) return 0; if (elem->length != oid_len) return 0; return memcmp(elem->value, oid, oid_len) == 0; } /* ── Time parsing ─────────────────────────────────────── */ /* Days before month (non-leap year, 0-indexed). */ static const int days_before_month[12] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334}; static int is_leap(int y) { return (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0); } /* Parse exactly n decimal digits from *p, advance *p. Returns -1 on error. */ static int parse_digits(const uint8_t **p, const uint8_t *end, int n) { int val = 0; for (int i = 0; i < n; i++) { if (*p >= end) return -1; uint8_t ch = **p; if (ch < '0' || ch > '9') return -1; val = val * 10 + (ch - '0'); (*p)++; } return val; } int litls_der_parse_time(const litls_der_element *elem, int64_t *ts) { const uint8_t *p = elem->value; const uint8_t *end = p + elem->length; int year, month, day, hour, minute, second; if (elem->tag == ASN1_UTCTIME) { /* YYMMDDHHMMSSZ */ year = parse_digits(&p, end, 2); if (year < 0) return -1; year += (year >= 50) ? 1900 : 2000; } else if (elem->tag == ASN1_GENERALIZEDTIME) { /* YYYYMMDDHHMMSSZ */ year = parse_digits(&p, end, 4); if (year < 0) return -1; } else { return -1; } month = parse_digits(&p, end, 2); if (month < 1 || month > 12) return -1; day = parse_digits(&p, end, 2); if (day < 1 || day > 31) return -1; hour = parse_digits(&p, end, 2); if (hour > 23) return -1; minute = parse_digits(&p, end, 2); if (minute > 59) return -1; second = parse_digits(&p, end, 2); if (second > 59) return -1; /* Trailing 'Z' expected but we don't enforce it strictly. */ /* Convert to Unix timestamp (UTC). */ int64_t days = 0; for (int y = 1970; y < year; y++) days += is_leap(y) ? 366 : 365; days += days_before_month[month - 1]; if (month > 2 && is_leap(year)) days++; days += day - 1; *ts = days * 86400 + hour * 3600 + minute * 60 + second; return 0; } /* ── DER writer ───────────────────────────────────────── */ void litls_der_writer_init(litls_der_writer *w, uint8_t *buf, size_t cap) { w->buf = buf; w->cap = cap; w->len = 0; w->err = 0; } /* Write raw bytes, set err on overflow. */ int litls_der_write_raw(litls_der_writer *w, const uint8_t *data, size_t len) { if (w->err) return -1; if (w->len + len > w->cap) { w->err = -1; return -1; } memcpy(w->buf + w->len, data, len); w->len += len; return 0; } /* Encode a DER length at position pos in w->buf. * Returns the number of bytes used, or 0 on error. * If dry_run, just returns the size without writing. */ static int der_encode_length(uint8_t *buf, size_t cap, size_t pos, size_t length) { if (length < 0x80) { if (pos < cap) buf[pos] = (uint8_t)length; return 1; } /* Determine number of length bytes needed. */ int n; if (length <= 0xFF) n = 1; else if (length <= 0xFFFF) n = 2; else if (length <= 0xFFFFFF) n = 3; else n = 4; if (pos + 1 + n > cap) return 0; buf[pos] = 0x80 | n; for (int i = n - 1; i >= 0; i--) buf[pos + 1 + i] = (uint8_t)(length >> (8 * (n - 1 - i))); return 1 + n; } int litls_der_write_tlv(litls_der_writer *w, uint8_t tag, const uint8_t *value, size_t length) { if (w->err) return -1; /* Tag (1 byte) + length encoding + value */ int lbytes_est = (length < 0x80) ? 1 : (length <= 0xFF) ? 2 : (length <= 0xFFFF) ? 3 : (length <= 0xFFFFFF) ? 4 : 5; size_t need = 1 + (size_t)lbytes_est + length; if (w->len + need > w->cap) { w->err = -1; return -1; } w->buf[w->len++] = tag; int lbytes = der_encode_length(w->buf, w->cap, w->len, length); if (!lbytes) { w->err = -1; return -1; } w->len += lbytes; if (length > 0) { memcpy(w->buf + w->len, value, length); w->len += length; } return 0; } int litls_der_write_oid(litls_der_writer *w, const uint8_t *oid, size_t oid_len) { return litls_der_write_tlv(w, ASN1_OID, oid, oid_len); } int litls_der_write_integer(litls_der_writer *w, const uint8_t *val, size_t val_len) { /* Strip leading zero bytes (but keep at least one). */ while (val_len > 1 && val[0] == 0x00) { val++; val_len--; } /* If high bit is set, prepend a 0x00 byte. */ int pad = (val[0] & 0x80) ? 1 : 0; size_t total = val_len + pad; if (w->err) return -1; size_t need = 1 + (total < 0x80 ? 1 : 2) + total; if (w->len + need > w->cap) { w->err = -1; return -1; } w->buf[w->len++] = ASN1_INTEGER; int lbytes = der_encode_length(w->buf, w->cap, w->len, total); if (!lbytes) { w->err = -1; return -1; } w->len += lbytes; if (pad) w->buf[w->len++] = 0x00; memcpy(w->buf + w->len, val, val_len); w->len += val_len; return 0; } /* Begin a constructed element. Writes tag + reserves 4 bytes for length. * Returns marker (position of the length field). */ static size_t der_begin_constructed(litls_der_writer *w, uint8_t tag) { if (w->err) return 0; if (w->len + 5 > w->cap) { w->err = -1; return 0; } w->buf[w->len++] = tag; size_t marker = w->len; /* Reserve 4 bytes for length (will be back-patched). */ w->len += 4; return marker; } size_t litls_der_begin_sequence(litls_der_writer *w) { return der_begin_constructed(w, ASN1_SEQUENCE); } size_t litls_der_begin_set(litls_der_writer *w) { return der_begin_constructed(w, ASN1_SET); } size_t litls_der_begin_context(litls_der_writer *w, int n) { return der_begin_constructed(w, 0xA0 | n); } int litls_der_end_constructed(litls_der_writer *w, size_t marker) { if (w->err) return -1; /* Content starts after the 4 reserved length bytes. */ size_t content_start = marker + 4; size_t content_len = w->len - content_start; /* Encode the real length. */ int lbytes = der_encode_length(w->buf, w->cap, marker, content_len); if (!lbytes) { w->err = -1; return -1; } /* If we used fewer than 4 bytes, shift the content left. */ if (lbytes < 4) { memmove(w->buf + marker + lbytes, w->buf + content_start, content_len); w->len = marker + lbytes + content_len; } return 0; } int litls_der_write_bit_string(litls_der_writer *w, const uint8_t *data, size_t data_len) { if (w->err) return -1; size_t total = 1 + data_len; /* unused-bits byte + data */ size_t need = 1 + (total < 0x80 ? 1 : (total <= 0xFF ? 2 : 3)) + total; if (w->len + need > w->cap) { w->err = -1; return -1; } w->buf[w->len++] = ASN1_BIT_STRING; int lbytes = der_encode_length(w->buf, w->cap, w->len, total); if (!lbytes) { w->err = -1; return -1; } w->len += lbytes; w->buf[w->len++] = 0x00; /* unused bits */ memcpy(w->buf + w->len, data, data_len); w->len += data_len; return 0; }