wolfssl/tests/api/create_x500_unique_id_certs.py

225 lines
7.9 KiB
Python

#!/usr/bin/env python3
"""
Generates tests/api/test_x500_unique_id_certs.h, the certificate blobs used
by test_wc_ParseCert_uniqueIdentifier() in tests/api.c.
Each certificate is self-signed ECDSA P-256 and carries an
x500UniqueIdentifier (OID 2.5.4.45) attribute in both the subject and the
issuer DN. That attribute is encoded as a BIT STRING rather than a
DirectoryString, which is what the test exercises. pyca/cryptography cannot
emit a BIT STRING DN attribute, so the TBSCertificate is built with the DER
helpers below and only the key generation and signing come from
cryptography.
The test parses with NO_VERIFY, so the validity dates are never checked and
the certificates do not need to be renewed when they expire. Adjust
NOT_BEFORE / NOT_AFTER below if new dates are wanted anyway.
Every run emits a fresh key and signature, so the byte arrays change even
when nothing else does. The DN contents the test asserts on (UNIQUE_ID and
EMBEDDED_NUL_ID) are unaffected.
Requires: pip install cryptography
Usage: python3 ./tests/api/create_x500_unique_id_certs.py
(run from the top of the wolfSSL source tree)
"""
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec
HEADER_PATH = './tests/api/test_x500_unique_id_certs.h'
HEADER_GUARD = 'WOLFCRYPT_TEST_X500_UNIQUE_ID_CERTS_H'
# UTCTime, YYMMDDHHMMSSZ. 49 maps to 2049 per RFC 5280 4.1.2.5.1.
NOT_BEFORE = '200101000000Z'
NOT_AFTER = '491231000000Z'
SERIAL = 0x0EAF12345678
COUNTRY = 'US'
ORG = 'Test Org'
COMMON_NAME = 'LEAF Test Card'
# Value of the x500UniqueIdentifier BIT STRING, byte aligned (0 unused bits).
UNIQUE_ID = bytes([0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF])
# Same, but with an embedded NUL that truncates the subject display string.
EMBEDDED_NUL_ID = bytes([0xAB, 0x00, 0xCD, 0xEF])
# An empty BIT STRING ('03 01 00'), which the parser must reject.
EMPTY_ID = b''
OID_COUNTRY = '2.5.4.6'
OID_ORG = '2.5.4.10'
OID_COMMON_NAME = '2.5.4.3'
OID_X500_UNIQUE_ID = '2.5.4.45'
OID_ECDSA_SHA256 = '1.2.840.10045.4.3.2'
LICENSE = """/* test_x500_unique_id_certs.h
*
* This file is generated automatically by running
* ./tests/api/create_x500_unique_id_certs.py.
*
* Copyright (C) 2006-2026 wolfSSL Inc.
*
* This file is part of wolfSSL.
*
* wolfSSL is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* wolfSSL is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
*/
"""
def tlv(tag: int, value: bytes) -> bytes:
if len(value) < 0x80:
length = bytes([len(value)])
else:
enc = len(value).to_bytes((len(value).bit_length() + 7) // 8, 'big')
length = bytes([0x80 | len(enc)]) + enc
return bytes([tag]) + length + value
def der_sequence(*items: bytes) -> bytes:
return tlv(0x30, b''.join(items))
def der_set(*items: bytes) -> bytes:
return tlv(0x31, b''.join(items))
def der_oid(dotted: str) -> bytes:
parts = [int(p) for p in dotted.split('.')]
body = bytes([parts[0] * 40 + parts[1]])
for part in parts[2:]:
chunk = [part & 0x7F]
part >>= 7
while part:
chunk.append((part & 0x7F) | 0x80)
part >>= 7
body += bytes(reversed(chunk))
return tlv(0x06, body)
def der_integer(value: int) -> bytes:
body = value.to_bytes(max(1, (value.bit_length() + 7) // 8), 'big')
if body[0] & 0x80:
body = b'\x00' + body
return tlv(0x02, body)
def der_printable_string(value: str) -> bytes:
return tlv(0x13, value.encode('ascii'))
def der_utf8_string(value: str) -> bytes:
return tlv(0x0C, value.encode('utf-8'))
def der_utc_time(value: str) -> bytes:
return tlv(0x17, value.encode('ascii'))
def der_bit_string(data: bytes, unused_bits: int = 0) -> bytes:
return tlv(0x03, bytes([unused_bits]) + data)
def der_explicit(number: int, data: bytes) -> bytes:
return tlv(0xA0 | number, data)
def rdn(oid: str, value: bytes) -> bytes:
"""A single-valued RelativeDistinguishedName."""
return der_set(der_sequence(der_oid(oid), value))
def name(unique_id: bytes) -> bytes:
return der_sequence(
rdn(OID_COUNTRY, der_printable_string(COUNTRY)),
rdn(OID_ORG, der_utf8_string(ORG)),
rdn(OID_COMMON_NAME, der_utf8_string(COMMON_NAME)),
rdn(OID_X500_UNIQUE_ID, der_bit_string(unique_id)),
)
def make_cert(unique_id: bytes) -> bytes:
key = ec.generate_private_key(ec.SECP256R1())
spki = key.public_key().public_bytes(
serialization.Encoding.DER,
serialization.PublicFormat.SubjectPublicKeyInfo)
sig_alg = der_sequence(der_oid(OID_ECDSA_SHA256))
dn = name(unique_id)
tbs = der_sequence(
der_explicit(0, der_integer(2)), # version v3
der_integer(SERIAL),
sig_alg,
dn, # issuer, self-signed
der_sequence(der_utc_time(NOT_BEFORE), der_utc_time(NOT_AFTER)),
dn, # subject
spki,
)
signature = key.sign(tbs, ec.ECDSA(hashes.SHA256()))
return der_sequence(tbs, sig_alg, der_bit_string(signature))
def pretty_date(utc_time: str) -> str:
"""YYMMDDHHMMSSZ -> YYYY-MM-DD, per RFC 5280 4.1.2.5.1."""
year = int(utc_time[0:2])
year += 1900 if year >= 50 else 2000
return '%04d-%s-%s' % (year, utc_time[2:4], utc_time[4:6])
def write_buffer(f, comment: str, name: str, data: bytes,
blank_after: bool = True) -> None:
f.write(comment)
f.write('static const unsigned char %s[] = {\n' % name)
for i in range(0, len(data), 12):
f.write(' ' + ', '.join('0x%02X' % b for b in data[i:i + 12]) +
(',' if i + 12 < len(data) else '') + '\n')
f.write('};\n')
if blank_after:
f.write('\n')
if __name__ == '__main__':
with open(HEADER_PATH, 'w') as f:
f.write(LICENSE)
f.write('\n#ifndef %s\n#define %s\n\n' % (HEADER_GUARD, HEADER_GUARD))
write_buffer(f,
'/* Self-signed ECDSA P-256 certificate with an\n'
' * x500UniqueIdentifier (OID 2.5.4.45) BIT STRING in both the\n'
' * subject and issuer DN. Valid %s to %s, but the tests\n'
' * parse with NO_VERIFY so the dates are never checked. */\n'
% (pretty_date(NOT_BEFORE), pretty_date(NOT_AFTER)),
'leafUniqueIdDer', make_cert(UNIQUE_ID))
write_buffer(f,
'/* Attribute value as stored, with the unused-bits octet\n'
' * stripped. */\n',
'expUniqueId', UNIQUE_ID)
write_buffer(f,
'/* Same certificate but with an embedded 0x00 in the value. The\n'
' * subject display string truncates at the NUL, but the\n'
' * WOLFSSL_X509_NAME entry must carry the full value. */\n',
'leafEmbeddedNulDer', make_cert(EMBEDDED_NUL_ID))
f.write('#ifdef OPENSSL_EXTRA\n')
write_buffer(f, '', 'expEmbeddedNul', EMBEDDED_NUL_ID, False)
f.write('#endif /* OPENSSL_EXTRA */\n\n')
f.write('#ifndef WOLFSSL_NO_ASN_STRICT\n')
write_buffer(f,
'/* Same certificate but with an empty BIT STRING value, which is\n'
' * rejected like a zero length DirectoryString. */\n',
'leafEmptyUniqueIdDer', make_cert(EMPTY_ID), False)
f.write('#endif /* !WOLFSSL_NO_ASN_STRICT */\n\n')
f.write('#endif /* %s */\n' % HEADER_GUARD)
print('wrote %s' % HEADER_PATH)