]>
Commit | Line | Data |
---|---|---|
dea01f66 CB |
1 | #!/usr/bin/env python3 |
2 | # | |
3 | # Libu2f-emu setup directory generator for USB U2F key emulation. | |
4 | # | |
5 | # Copyright (c) 2020 César Belley <[email protected]> | |
6 | # Written by César Belley <[email protected]> | |
7 | # | |
8 | # This work is licensed under the terms of the GNU GPL, version 2 | |
9 | # or, at your option, any later version. See the COPYING file in | |
10 | # the top-level directory. | |
11 | ||
12 | import sys | |
13 | import os | |
14 | from random import randint | |
15 | from typing import Tuple | |
16 | ||
17 | from cryptography.hazmat.backends import default_backend | |
18 | from cryptography.hazmat.primitives.asymmetric import ec | |
19 | from cryptography.hazmat.primitives.serialization import Encoding, \ | |
20 | NoEncryption, PrivateFormat, PublicFormat | |
21 | from OpenSSL import crypto | |
22 | ||
23 | ||
24 | def write_setup_dir(dirpath: str, privkey_pem: bytes, cert_pem: bytes, | |
25 | entropy: bytes, counter: int) -> None: | |
26 | """ | |
27 | Write the setup directory. | |
28 | ||
29 | Args: | |
30 | dirpath: The directory path. | |
31 | key_pem: The private key PEM. | |
32 | cert_pem: The certificate PEM. | |
33 | entropy: The 48 bytes of entropy. | |
34 | counter: The counter value. | |
35 | """ | |
36 | # Directory | |
37 | os.mkdir(dirpath) | |
38 | ||
39 | # Private key | |
40 | with open(f'{dirpath}/private-key.pem', 'bw') as f: | |
41 | f.write(privkey_pem) | |
42 | ||
43 | # Certificate | |
44 | with open(f'{dirpath}/certificate.pem', 'bw') as f: | |
45 | f.write(cert_pem) | |
46 | ||
47 | # Entropy | |
48 | with open(f'{dirpath}/entropy', 'wb') as f: | |
49 | f.write(entropy) | |
50 | ||
51 | # Counter | |
52 | with open(f'{dirpath}/counter', 'w') as f: | |
53 | f.write(f'{str(counter)}\n') | |
54 | ||
55 | ||
56 | def generate_ec_key_pair() -> Tuple[str, str]: | |
57 | """ | |
58 | Generate an ec key pair. | |
59 | ||
60 | Returns: | |
61 | The private and public key PEM. | |
62 | """ | |
63 | # Key generation | |
64 | privkey = ec.generate_private_key(ec.SECP256R1, default_backend()) | |
65 | pubkey = privkey.public_key() | |
66 | ||
67 | # PEM serialization | |
68 | privkey_pem = privkey.private_bytes(encoding=Encoding.PEM, | |
69 | format=PrivateFormat.TraditionalOpenSSL, | |
70 | encryption_algorithm=NoEncryption()) | |
71 | pubkey_pem = pubkey.public_bytes(encoding=Encoding.PEM, | |
72 | format=PublicFormat.SubjectPublicKeyInfo) | |
73 | return privkey_pem, pubkey_pem | |
74 | ||
75 | ||
76 | def generate_certificate(privkey_pem: str, pubkey_pem: str) -> str: | |
77 | """ | |
78 | Generate a x509 certificate from a key pair. | |
79 | ||
80 | Args: | |
81 | privkey_pem: The private key PEM. | |
82 | pubkey_pem: The public key PEM. | |
83 | ||
84 | Returns: | |
85 | The certificate PEM. | |
86 | """ | |
87 | # Convert key pair | |
88 | privkey = crypto.load_privatekey(crypto.FILETYPE_PEM, privkey_pem) | |
89 | pubkey = crypto.load_publickey(crypto.FILETYPE_PEM, pubkey_pem) | |
90 | ||
91 | # New x509v3 certificate | |
92 | cert = crypto.X509() | |
93 | cert.set_version(0x2) | |
94 | ||
95 | # Serial number | |
96 | cert.set_serial_number(randint(1, 2 ** 64)) | |
97 | ||
98 | # Before / After | |
99 | cert.gmtime_adj_notBefore(0) | |
100 | cert.gmtime_adj_notAfter(4 * (365 * 24 * 60 * 60)) | |
101 | ||
102 | # Public key | |
103 | cert.set_pubkey(pubkey) | |
104 | ||
105 | # Subject name and issueer | |
106 | cert.get_subject().CN = "U2F emulated" | |
107 | cert.set_issuer(cert.get_subject()) | |
108 | ||
109 | # Extensions | |
110 | cert.add_extensions([ | |
111 | crypto.X509Extension(b"subjectKeyIdentifier", | |
112 | False, b"hash", subject=cert), | |
113 | ]) | |
114 | cert.add_extensions([ | |
115 | crypto.X509Extension(b"authorityKeyIdentifier", | |
116 | False, b"keyid:always", issuer=cert), | |
117 | ]) | |
118 | cert.add_extensions([ | |
119 | crypto.X509Extension(b"basicConstraints", True, b"CA:TRUE") | |
120 | ]) | |
121 | ||
122 | # Signature | |
123 | cert.sign(privkey, 'sha256') | |
124 | ||
125 | return crypto.dump_certificate(crypto.FILETYPE_PEM, cert) | |
126 | ||
127 | ||
128 | def generate_setup_dir(dirpath: str) -> None: | |
129 | """ | |
130 | Generates the setup directory. | |
131 | ||
132 | Args: | |
133 | dirpath: The directory path. | |
134 | """ | |
135 | # Key pair | |
136 | privkey_pem, pubkey_pem = generate_ec_key_pair() | |
137 | ||
138 | # Certificate | |
139 | certificate_pem = generate_certificate(privkey_pem, pubkey_pem) | |
140 | ||
141 | # Entropy | |
142 | entropy = os.urandom(48) | |
143 | ||
144 | # Counter | |
145 | counter = 0 | |
146 | ||
147 | # Write | |
148 | write_setup_dir(dirpath, privkey_pem, certificate_pem, entropy, counter) | |
149 | ||
150 | ||
151 | def main() -> None: | |
152 | """ | |
153 | Main function | |
154 | """ | |
155 | # Dir path | |
156 | if len(sys.argv) != 2: | |
157 | sys.stderr.write(f'Usage: {sys.argv[0]} <setup_dir>\n') | |
158 | exit(2) | |
159 | dirpath = sys.argv[1] | |
160 | ||
161 | # Dir non existence | |
162 | if os.path.exists(dirpath): | |
163 | sys.stderr.write(f'Directory: {dirpath} already exists.\n') | |
164 | exit(1) | |
165 | ||
166 | generate_setup_dir(dirpath) | |
167 | ||
168 | ||
169 | if __name__ == '__main__': | |
170 | main() |