|
from __future__ import division |
|
|
|
import binascii |
|
import base64 |
|
import warnings |
|
from itertools import chain |
|
from six import int2byte, text_type |
|
from ._compat import compat26_str, str_idx_as_int |
|
|
|
|
|
class UnexpectedDER(Exception): |
|
pass |
|
|
|
|
|
def encode_constructed(tag, value): |
|
return int2byte(0xA0 + tag) + encode_length(len(value)) + value |
|
|
|
|
|
def encode_integer(r): |
|
assert r >= 0 |
|
h = ("%x" % r).encode() |
|
if len(h) % 2: |
|
h = b"0" + h |
|
s = binascii.unhexlify(h) |
|
num = str_idx_as_int(s, 0) |
|
if num <= 0x7F: |
|
return b"\x02" + encode_length(len(s)) + s |
|
else: |
|
|
|
|
|
|
|
return b"\x02" + encode_length(len(s) + 1) + b"\x00" + s |
|
|
|
|
|
|
|
|
|
_sentry = object() |
|
|
|
|
|
def encode_bitstring(s, unused=_sentry): |
|
""" |
|
Encode a binary string as a BIT STRING using :term:`DER` encoding. |
|
|
|
Note, because there is no native Python object that can encode an actual |
|
bit string, this function only accepts byte strings as the `s` argument. |
|
The byte string is the actual bit string that will be encoded, padded |
|
on the right (least significant bits, looking from big endian perspective) |
|
to the first full byte. If the bit string has a bit length that is multiple |
|
of 8, then the padding should not be included. For correct DER encoding |
|
the padding bits MUST be set to 0. |
|
|
|
Number of bits of padding need to be provided as the `unused` parameter. |
|
In case they are specified as None, it means the number of unused bits |
|
is already encoded in the string as the first byte. |
|
|
|
The deprecated call convention specifies just the `s` parameters and |
|
encodes the number of unused bits as first parameter (same convention |
|
as with None). |
|
|
|
Empty string must be encoded with `unused` specified as 0. |
|
|
|
Future version of python-ecdsa will make specifying the `unused` argument |
|
mandatory. |
|
|
|
:param s: bytes to encode |
|
:type s: bytes like object |
|
:param unused: number of bits at the end of `s` that are unused, must be |
|
between 0 and 7 (inclusive) |
|
:type unused: int or None |
|
|
|
:raises ValueError: when `unused` is too large or too small |
|
|
|
:return: `s` encoded using DER |
|
:rtype: bytes |
|
""" |
|
encoded_unused = b"" |
|
len_extra = 0 |
|
if unused is _sentry: |
|
warnings.warn( |
|
"Legacy call convention used, unused= needs to be specified", |
|
DeprecationWarning, |
|
) |
|
elif unused is not None: |
|
if not 0 <= unused <= 7: |
|
raise ValueError("unused must be integer between 0 and 7") |
|
if unused: |
|
if not s: |
|
raise ValueError("unused is non-zero but s is empty") |
|
last = str_idx_as_int(s, -1) |
|
if last & (2**unused - 1): |
|
raise ValueError("unused bits must be zeros in DER") |
|
encoded_unused = int2byte(unused) |
|
len_extra = 1 |
|
return b"\x03" + encode_length(len(s) + len_extra) + encoded_unused + s |
|
|
|
|
|
def encode_octet_string(s): |
|
return b"\x04" + encode_length(len(s)) + s |
|
|
|
|
|
def encode_oid(first, second, *pieces): |
|
assert 0 <= first < 2 and 0 <= second <= 39 or first == 2 and 0 <= second |
|
body = b"".join( |
|
chain( |
|
[encode_number(40 * first + second)], |
|
(encode_number(p) for p in pieces), |
|
) |
|
) |
|
return b"\x06" + encode_length(len(body)) + body |
|
|
|
|
|
def encode_sequence(*encoded_pieces): |
|
total_len = sum([len(p) for p in encoded_pieces]) |
|
return b"\x30" + encode_length(total_len) + b"".join(encoded_pieces) |
|
|
|
|
|
def encode_number(n): |
|
b128_digits = [] |
|
while n: |
|
b128_digits.insert(0, (n & 0x7F) | 0x80) |
|
n = n >> 7 |
|
if not b128_digits: |
|
b128_digits.append(0) |
|
b128_digits[-1] &= 0x7F |
|
return b"".join([int2byte(d) for d in b128_digits]) |
|
|
|
|
|
def is_sequence(string): |
|
return string and string[:1] == b"\x30" |
|
|
|
|
|
def remove_constructed(string): |
|
s0 = str_idx_as_int(string, 0) |
|
if (s0 & 0xE0) != 0xA0: |
|
raise UnexpectedDER( |
|
"wanted type 'constructed tag' (0xa0-0xbf), got 0x%02x" % s0 |
|
) |
|
tag = s0 & 0x1F |
|
length, llen = read_length(string[1:]) |
|
body = string[1 + llen : 1 + llen + length] |
|
rest = string[1 + llen + length :] |
|
return tag, body, rest |
|
|
|
|
|
def remove_sequence(string): |
|
if not string: |
|
raise UnexpectedDER("Empty string does not encode a sequence") |
|
if string[:1] != b"\x30": |
|
n = str_idx_as_int(string, 0) |
|
raise UnexpectedDER("wanted type 'sequence' (0x30), got 0x%02x" % n) |
|
length, lengthlength = read_length(string[1:]) |
|
if length > len(string) - 1 - lengthlength: |
|
raise UnexpectedDER("Length longer than the provided buffer") |
|
endseq = 1 + lengthlength + length |
|
return string[1 + lengthlength : endseq], string[endseq:] |
|
|
|
|
|
def remove_octet_string(string): |
|
if string[:1] != b"\x04": |
|
n = str_idx_as_int(string, 0) |
|
raise UnexpectedDER("wanted type 'octetstring' (0x04), got 0x%02x" % n) |
|
length, llen = read_length(string[1:]) |
|
body = string[1 + llen : 1 + llen + length] |
|
rest = string[1 + llen + length :] |
|
return body, rest |
|
|
|
|
|
def remove_object(string): |
|
if not string: |
|
raise UnexpectedDER( |
|
"Empty string does not encode an object identifier" |
|
) |
|
if string[:1] != b"\x06": |
|
n = str_idx_as_int(string, 0) |
|
raise UnexpectedDER("wanted type 'object' (0x06), got 0x%02x" % n) |
|
length, lengthlength = read_length(string[1:]) |
|
body = string[1 + lengthlength : 1 + lengthlength + length] |
|
rest = string[1 + lengthlength + length :] |
|
if not body: |
|
raise UnexpectedDER("Empty object identifier") |
|
if len(body) != length: |
|
raise UnexpectedDER( |
|
"Length of object identifier longer than the provided buffer" |
|
) |
|
numbers = [] |
|
while body: |
|
n, ll = read_number(body) |
|
numbers.append(n) |
|
body = body[ll:] |
|
n0 = numbers.pop(0) |
|
if n0 < 80: |
|
first = n0 // 40 |
|
else: |
|
first = 2 |
|
second = n0 - (40 * first) |
|
numbers.insert(0, first) |
|
numbers.insert(1, second) |
|
return tuple(numbers), rest |
|
|
|
|
|
def remove_integer(string): |
|
if not string: |
|
raise UnexpectedDER( |
|
"Empty string is an invalid encoding of an integer" |
|
) |
|
if string[:1] != b"\x02": |
|
n = str_idx_as_int(string, 0) |
|
raise UnexpectedDER("wanted type 'integer' (0x02), got 0x%02x" % n) |
|
length, llen = read_length(string[1:]) |
|
if length > len(string) - 1 - llen: |
|
raise UnexpectedDER("Length longer than provided buffer") |
|
if length == 0: |
|
raise UnexpectedDER("0-byte long encoding of integer") |
|
numberbytes = string[1 + llen : 1 + llen + length] |
|
rest = string[1 + llen + length :] |
|
msb = str_idx_as_int(numberbytes, 0) |
|
if not msb < 0x80: |
|
raise UnexpectedDER("Negative integers are not supported") |
|
|
|
if length > 1 and not msb: |
|
|
|
|
|
smsb = str_idx_as_int(numberbytes, 1) |
|
if smsb < 0x80: |
|
raise UnexpectedDER( |
|
"Invalid encoding of integer, unnecessary " |
|
"zero padding bytes" |
|
) |
|
return int(binascii.hexlify(numberbytes), 16), rest |
|
|
|
|
|
def read_number(string): |
|
number = 0 |
|
llen = 0 |
|
if str_idx_as_int(string, 0) == 0x80: |
|
raise UnexpectedDER("Non minimal encoding of OID subidentifier") |
|
|
|
|
|
while True: |
|
if llen >= len(string): |
|
raise UnexpectedDER("ran out of length bytes") |
|
number = number << 7 |
|
d = str_idx_as_int(string, llen) |
|
number += d & 0x7F |
|
llen += 1 |
|
if not d & 0x80: |
|
break |
|
return number, llen |
|
|
|
|
|
def encode_length(l): |
|
assert l >= 0 |
|
if l < 0x80: |
|
return int2byte(l) |
|
s = ("%x" % l).encode() |
|
if len(s) % 2: |
|
s = b"0" + s |
|
s = binascii.unhexlify(s) |
|
llen = len(s) |
|
return int2byte(0x80 | llen) + s |
|
|
|
|
|
def read_length(string): |
|
if not string: |
|
raise UnexpectedDER("Empty string can't encode valid length value") |
|
num = str_idx_as_int(string, 0) |
|
if not (num & 0x80): |
|
|
|
return (num & 0x7F), 1 |
|
|
|
|
|
llen = num & 0x7F |
|
if not llen: |
|
raise UnexpectedDER("Invalid length encoding, length of length is 0") |
|
if llen > len(string) - 1: |
|
raise UnexpectedDER("Length of length longer than provided buffer") |
|
|
|
msb = str_idx_as_int(string, 1) |
|
if not msb or llen == 1 and msb < 0x80: |
|
raise UnexpectedDER("Not minimal encoding of length") |
|
return int(binascii.hexlify(string[1 : 1 + llen]), 16), 1 + llen |
|
|
|
|
|
def remove_bitstring(string, expect_unused=_sentry): |
|
""" |
|
Remove a BIT STRING object from `string` following :term:`DER`. |
|
|
|
The `expect_unused` can be used to specify if the bit string should |
|
have the amount of unused bits decoded or not. If it's an integer, any |
|
read BIT STRING that has number of unused bits different from specified |
|
value will cause UnexpectedDER exception to be raised (this is especially |
|
useful when decoding BIT STRINGS that have DER encoded object in them; |
|
DER encoding is byte oriented, so the unused bits will always equal 0). |
|
|
|
If the `expect_unused` is specified as None, the first element returned |
|
will be a tuple, with the first value being the extracted bit string |
|
while the second value will be the decoded number of unused bits. |
|
|
|
If the `expect_unused` is unspecified, the decoding of byte with |
|
number of unused bits will not be attempted and the bit string will be |
|
returned as-is, the callee will be required to decode it and verify its |
|
correctness. |
|
|
|
Future version of python will require the `expected_unused` parameter |
|
to be specified. |
|
|
|
:param string: string of bytes to extract the BIT STRING from |
|
:type string: bytes like object |
|
:param expect_unused: number of bits that should be unused in the BIT |
|
STRING, or None, to return it to caller |
|
:type expect_unused: int or None |
|
|
|
:raises UnexpectedDER: when the encoding does not follow DER. |
|
|
|
:return: a tuple with first element being the extracted bit string and |
|
the second being the remaining bytes in the string (if any); if the |
|
`expect_unused` is specified as None, the first element of the returned |
|
tuple will be a tuple itself, with first element being the bit string |
|
as bytes and the second element being the number of unused bits at the |
|
end of the byte array as an integer |
|
:rtype: tuple |
|
""" |
|
if not string: |
|
raise UnexpectedDER("Empty string does not encode a bitstring") |
|
if expect_unused is _sentry: |
|
warnings.warn( |
|
"Legacy call convention used, expect_unused= needs to be" |
|
" specified", |
|
DeprecationWarning, |
|
) |
|
num = str_idx_as_int(string, 0) |
|
if string[:1] != b"\x03": |
|
raise UnexpectedDER("wanted bitstring (0x03), got 0x%02x" % num) |
|
length, llen = read_length(string[1:]) |
|
if not length: |
|
raise UnexpectedDER("Invalid length of bit string, can't be 0") |
|
body = string[1 + llen : 1 + llen + length] |
|
rest = string[1 + llen + length :] |
|
if expect_unused is not _sentry: |
|
unused = str_idx_as_int(body, 0) |
|
if not 0 <= unused <= 7: |
|
raise UnexpectedDER("Invalid encoding of unused bits") |
|
if expect_unused is not None and expect_unused != unused: |
|
raise UnexpectedDER("Unexpected number of unused bits") |
|
body = body[1:] |
|
if unused: |
|
if not body: |
|
raise UnexpectedDER("Invalid encoding of empty bit string") |
|
last = str_idx_as_int(body, -1) |
|
|
|
if last & (2**unused - 1): |
|
raise UnexpectedDER("Non zero padding bits in bit string") |
|
if expect_unused is None: |
|
body = (body, unused) |
|
return body, rest |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def unpem(pem): |
|
if isinstance(pem, text_type): |
|
pem = pem.encode() |
|
|
|
d = b"".join( |
|
[ |
|
l.strip() |
|
for l in pem.split(b"\n") |
|
if l and not l.startswith(b"-----") |
|
] |
|
) |
|
return base64.b64decode(d) |
|
|
|
|
|
def topem(der, name): |
|
b64 = base64.b64encode(compat26_str(der)) |
|
lines = [("-----BEGIN %s-----\n" % name).encode()] |
|
lines.extend( |
|
[b64[start : start + 76] + b"\n" for start in range(0, len(b64), 76)] |
|
) |
|
lines.append(("-----END %s-----\n" % name).encode()) |
|
return b"".join(lines) |
|
|