mirror of https://github.com/wolfSSL/wolfssl.git
225 lines
7.9 KiB
Python
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)
|