├── .dockerignore ├── .gitignore ├── ChangeLog ├── Dockerfile ├── LICENSE ├── README.md ├── requirements.txt ├── serial ├── src ├── Certificate.py ├── Definitions.py ├── Terminal.py ├── Test.py ├── TestCases.py ├── TestExpander.py ├── TestFunctionality.py ├── TestGroups.py ├── TestOverflow.py ├── TestServer.py ├── TestSet.py └── __init__.py └── x509test.py /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | docs 3 | 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### https://raw.github.com/github/gitignore/14b7566ce157ce95b07006466bacee160f242284/Python.gitignore 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | env/ 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .cache 41 | nosetests.xml 42 | coverage.xml 43 | 44 | # Translations 45 | *.mo 46 | *.pot 47 | 48 | # Django stuff: 49 | *.log 50 | 51 | # Sphinx documentation 52 | docs/_build/ 53 | 54 | # PyBuilder 55 | target/ 56 | 57 | 58 | -------------------------------------------------------------------------------- /ChangeLog: -------------------------------------------------------------------------------- 1 | Software 2 | 0.2.3 3 | : Minor update 4 | + Added md5 check 5 | = Modified functionality component 6 | = Expanded include-only to other components 7 | 0.2.2 8 | : Minor update 9 | = Modified overflow component 10 | = Minor bug fixes 11 | 0.2.1 12 | : Minor update 13 | + Added time gap configuration 14 | = Modified overflow component 15 | 0.2.0 16 | : First release 17 | + Added overflow component 18 | = Source code sytle improvement 19 | = Minor bug fixes 20 | 0.1.0 21 | : Initial Beta version 22 | 23 | TestCase 24 | 1.0 25 | : Initial set of test cases 26 | 1.1 27 | : Minor update 28 | + Added UnknownNonCriticalExtension test case 29 | = Changed the name of a few test cases 30 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM dockerfile/python:latest 2 | MAINTAINER dweinstein 3 | 4 | ENV DEBIAN_FRONTEND noninteractive 5 | 6 | RUN apt-get update && \ 7 | apt-get install -qq -y --no-install-recommends openssl build-essential libssl-dev libffi-dev \ 8 | python3-dev python3-pip 9 | 10 | ADD requirements.txt /tmp/requirements.txt 11 | RUN cd /tmp && pip3 install -r requirements.txt 12 | 13 | WORKDIR /opt/app 14 | ENTRYPOINT ["python3", "/opt/app/x509test.py"] 15 | ADD . /opt/app/ 16 | 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Calvin Liang 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | x509test 2 | ======== 3 | 4 | If you have any questions, suggestions, comments, concerns, or interesting stories, please email . 5 | 6 | Description: 7 | 8 | x509test is a software written in Python 3 that test the x509 certificate verification process of the target SSL/TLS client. The inspiration of this software comes from multiple reports on the insecurity of a SSL/TLS client due to incorrect verification of x509 certificate chain. This phenomenon is caused by many factors. One of which is the lack of negative feedback from over-acceptance of invalid certificates. This software is an attempt to increase the security of a client-side SSL/TLS software by providing negative feedbacks to the developers. 9 | 10 | Test Procedure: 11 | 12 | 1. The software takes in a user-supplied fqdn, where the fqdn is the destination of the client connection 13 | 14 | 2. The software reads the certificate and key of the root CA. If no root CA is specified, the software generate a self-signed certificate that acts as the root CA. 15 | (NOTE: the root certificate must be trusted by the client software; either by including it to the OS’s trust store or manually configure the client software to trust the certificate.) 16 | 17 | 3. The software generates a set of test certificates. Some are signed directly by the root CA while others are chained with other intermediate CAs. The majority of the test certificates contain flaws. 18 | 19 | 4. The software starts a SSL/TLS server and waits for a client to connect. Each session corresponds to a single test certificate chain. If the client completes the handshake procedure with an invalid certificate chain, or terminates the handshake procedure with a valid certificate chain, then the software will denote such behavior as a potential violation. Regardless of the outcome, the software always terminates the connection once result is obtained and starts a new session with a different test certificate chain. 20 | (NOTE: some ports require root privilege, so it is recommended to run this software in root.) 21 | 22 | 5. Results will be printed to the terminal, or a file if specified, as the test progresses. There are only three possible results from a given test. Pass means no non-compliance behavior is observed; fail means non-compliance behavior encountered; unsupported means the underlying system in which x509test is running on does not support the particular test. 23 | 24 | Dependencies: 25 | 26 | Python 3.2 27 | pyOpenSSL 0.14 28 | pyasn1 0.1.7 29 | pyasn1_modules 0.0.5 30 | OpenSSL 1.0.1 31 | 32 | Installation: 33 | 34 | Currently, no installation procedure is needed. After all dependencies are installed, simply go to the X509Test folder and run x509test.py using python interpreter to start the program. 35 | 36 | Example Run: 37 | 38 | All following examples use www.tls.test as the fqdn, which means it is pretending to be the server of the (fake) site www.tls.test. 39 | 40 | All following examples assume Linux-based OS. Windows users should run the command prompt as administrator (equivalent of sudo) and specify the path to your python3.exe executable file (equivalent of python3). 41 | 42 | All following examples assume the current working directory is X509Test (the downloaded folder that contains x509test.py and other items.) 43 | 44 | Please make sure that no other service is using the same port that you are about to use. 45 | 46 | 1. A server listens on port 443 with an IPv4 address of 10.1.2.3: 47 | sudo python3 x509test.py www.tls.test -a 10.1.2.3 -p 443 48 | 49 | 2. A server listens on port 8080 with a loop back address, and rebuild all test cases: 50 | sudo python3 x509test.py www.tls.test -r -p 8080 51 | 52 | 3. List all available test cases (fqdn can be any string): 53 | python3 x509test.py fqdn -l 54 | 55 | 4. Run functionality test only: 56 | sudo python3 x509test.py www.tls.test -c func 57 | 58 | 5. Run both functionality and certificate tests with SSL3: 59 | sudo python3 x509test.py www.tls.test -c full --ssl SSLv3 60 | 61 | 6. The root certificate is encrypted with password 'secret': 62 | sudo python3 x509test.py www.tls.test --ca-password secret 63 | 64 | 7. Print the current version and license of the software (fqdn can be any string): 65 | python3 x509test.py fqdn --version 66 | 67 | More options can be found by using --help: 68 | python3 x509test.py fqdn --help 69 | 70 | Why use x509test: 71 | 72 | 1. Security is hard 73 | 2. x509test is easy to use 74 | 3. x509test is open-source 75 | 4. x509test is free 76 | 77 | 78 | Thank you for using x509test. 79 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyOpenSSL==0.14 2 | pyasn1==0.1.7 3 | pyasn1_modules==0.0.5 4 | 5 | -------------------------------------------------------------------------------- /serial: -------------------------------------------------------------------------------- 1 | 1001 2 | -------------------------------------------------------------------------------- /src/Certificate.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file contains all generation logic and data structure relating to the 3 | X509 certificate. Class Certificate represents a single certificate. Other 4 | peripheral classes supply information to the Certificate object. 5 | 6 | @author: Calvin Jia Liang 7 | Created on May 15, 2014 8 | """ 9 | 10 | from pyasn1.codec.der import decoder, encoder 11 | from pyasn1_modules import rfc2459 12 | 13 | from src.Definitions import * 14 | 15 | # Certificate subject class that represents the subject distinguish name. 16 | 17 | 18 | class CertSubj: 19 | 20 | """ 21 | CertSubj constructor 22 | :param commonName: common name (CN) 23 | :type commonName: string 24 | :param country: country (C) 25 | :type country: string 26 | :param state: state (ST) 27 | :type state: string 28 | :param city: location (L) 29 | :type city: string 30 | :param org: organization (O) 31 | :type org: string 32 | :param unit: unit (U) 33 | :type unit: string 34 | :param email: email 35 | :type email: string 36 | :returns: CertSubj object 37 | """ 38 | 39 | def __init__(self, commonName, country=DEFAULT_C, state=DEFAULT_ST, 40 | city=DEFAULT_L, org=DEFAULT_O, unit=DEFAULT_U, 41 | email=DEFAULT_EMAIL): 42 | 43 | self.commonName = commonName 44 | self.country = country 45 | self.state = state 46 | self.city = city 47 | self.org = org 48 | self.unit = unit 49 | self.email = email 50 | 51 | """ 52 | Build all missing, or computationally expensive, components in this object 53 | :returns: CertSubj object 54 | """ 55 | 56 | def build(self): 57 | return self 58 | 59 | """ 60 | Get information in this object in pyOpenSSL format 61 | :returns: pyOpenSSL Subject object 62 | """ 63 | 64 | def getSubject(self): 65 | subj = crypto.X509().get_subject() 66 | subj.C = self.country 67 | subj.ST = self.state 68 | subj.L = self.city 69 | subj.O = self.org 70 | subj.OU = self.unit 71 | subj.CN = self.commonName 72 | subj.emailAddress = self.email 73 | 74 | return subj 75 | 76 | # Certificate key class that represents the public/private key pair. 77 | 78 | 79 | class CertKey: 80 | 81 | """ 82 | CertSubj constructor 83 | :param key: public/private key pairs 84 | :type key: pyOpenSSL key object 85 | :param kSize: length of the key 86 | :type kSize: integer 87 | :param kType: type of the key 88 | :type kType: pyOpenSSL macro 89 | :returns: CertKey object 90 | """ 91 | 92 | def __init__(self, key=None, kSize=DEFAULT_KSIZE, kType=DEFAULT_KTYPE): 93 | self.key = key 94 | self.kSize = kSize 95 | self.kType = kType 96 | 97 | """ 98 | Build all missing, or computationally expensive, components in this object 99 | :returns: CertKey object 100 | """ 101 | 102 | def build(self): 103 | if (not self.key): 104 | self.key = crypto.PKey() 105 | self.key.generate_key(self.kType, self.kSize) 106 | 107 | return self 108 | 109 | # Certificate security class that holds the subject public key and 110 | # other miscellaneous information. 111 | 112 | 113 | class CertSec: 114 | 115 | """ 116 | CertSec constructor 117 | :param fqdn: fully quantifiable domain name 118 | :type fqdn: string 119 | :param notBefore: hours (absolute value) before NOW when the validity begins 120 | :type notBefore: integer 121 | :param notAfter: hours (absolute value) after NOW when the validity ends 122 | :type notAfter: integer 123 | :param key: key of this certificate 124 | :type key: CertKey object 125 | :param kSize: length of the key 126 | :type kSize: integer 127 | :param kType: type of the key 128 | :type kType: pyOpenSSL macro 129 | :param version: X509 certificate version 130 | :type version: pyOpenSSL macro 131 | :param digest: type of the hash algorithm 132 | :type digest: string 133 | :param serial: serial number of the certificate 134 | :type serial: integer 135 | :returns: CertSec object 136 | """ 137 | 138 | def __init__( 139 | self, fqdn, notBefore=DEFAULT_HOUR_BEFORE, 140 | notAfter=DEFAULT_HOUR_AFTER, key=None, kSize=DEFAULT_KSIZE, 141 | kType=DEFAULT_KTYPE, version=DEFAULT_VERSION, 142 | digest=DEFAULT_DIGEST, serial=None): 143 | 144 | self.fqdn = fqdn 145 | self.digest = digest 146 | self.serial = serial 147 | self.version = version 148 | self.notBefore = notBefore 149 | self.notAfter = notAfter 150 | self.serial = serial if serial else getNewSerial() 151 | self.certKey = CertKey(key, kSize, kType) 152 | 153 | """ 154 | Get the actual keys used in the certificate 155 | :returns: pyOpenSSL Key object 156 | """ 157 | 158 | def getKey(self): 159 | return self.certKey.key 160 | 161 | """ 162 | Build all missing, or computationally expensive, components in this object 163 | :returns: CertSec object 164 | """ 165 | 166 | def build(self): 167 | self.certKey.build() 168 | return self 169 | 170 | # Certificate signer class that holds the signer's information. 171 | 172 | 173 | class CertSign: 174 | 175 | """ 176 | CertSign constructor 177 | :param signPathPrefix: path prefix of the signer's cert and key files in PEM 178 | :type signPathPrefix: string 179 | :param signKey: private key of the signer 180 | :type signKey: pyOpenSSL pkey object 181 | :param signSubj: distinguish name of the signer 182 | :type signSubj: pyOpenSSL subject object 183 | :param keyPassword: password of the key file (only used when signKey==None) 184 | :type keyPassword: string 185 | :returns: CertSign object 186 | """ 187 | 188 | def __init__( 189 | self, signPathPrefix, signKey=None, signSubj=None, 190 | keyPassword=DEFAULT_PASSWORD): 191 | self.signPathPrefix = signPathPrefix 192 | self.signKey = signKey 193 | self.signSubj = signSubj 194 | 195 | self.keyPassword = keyPassword.encode('utf-8') 196 | 197 | """ 198 | Get the actual keys used for signature 199 | :returns: pyOpenSSL Key object 200 | """ 201 | 202 | def getKey(self): 203 | return self.signKey.key 204 | 205 | """ 206 | Build all missing, or computationally expensive, components in this object 207 | :returns: CertSign object 208 | """ 209 | 210 | def build(self): 211 | if (not self.signKey): 212 | keyPath = self.signPathPrefix + ".key" 213 | with open(keyPath, 'rb') as f: 214 | key = crypto.load_privatekey( 215 | crypto.FILETYPE_PEM, 216 | f.read(), 217 | self.keyPassword) 218 | self.signKey = CertKey(key, None, None) 219 | 220 | if (not self.signSubj): 221 | crtPath = self.signPathPrefix + ".crt" 222 | with open(crtPath, 'rb') as f: 223 | self.signSubj = crypto.load_certificate( 224 | crypto.FILETYPE_PEM, 225 | f.read()).get_subject() 226 | 227 | return self 228 | 229 | # Abstract class that represents a generic certificate extension type. 230 | 231 | 232 | class CertExt: 233 | 234 | """ 235 | CertExt constructor 236 | :param critical: assert if the extension is critical 237 | :type critical: boolean 238 | :returns: CertExt object 239 | """ 240 | 241 | def __init__(self, critical=False): 242 | self.critical = critical 243 | 244 | """ 245 | Check if the extension is critical 246 | :returns: boolean 247 | """ 248 | 249 | def criticality(self): 250 | return self.critical 251 | 252 | """ 253 | Get the name of this extension in pyOpenSSL format 254 | :returns: bytes 255 | """ 256 | 257 | def name(self): 258 | pass 259 | 260 | """ 261 | Get the value of this extension in pyOpenSSL format 262 | :returns: bytes 263 | """ 264 | 265 | def value(self): 266 | pass 267 | 268 | # Basic constraint class that represents the RFC5280 4.2.1.9 extension. 269 | 270 | 271 | class BasicConstraint(CertExt): 272 | 273 | """ 274 | BasicConstraint constructor 275 | :param ca: assert if the certificate represents a CA 276 | :type ca: boolean 277 | :param pathLen: the maximum depth in which the CA is able to sign descendant CA 278 | :type pathLen: string 279 | :param critical: assert if the extension is critical 280 | :type critical: boolean 281 | :returns: BasicConstraint object 282 | """ 283 | 284 | def __init__(self, ca, pathLen=None, critical=True): 285 | super(self.__class__, self).__init__(critical) 286 | self.ca = ca 287 | self.pathLen = pathLen 288 | 289 | """ 290 | See CertExt 291 | """ 292 | 293 | def name(self): 294 | return b"basicConstraints" 295 | 296 | """ 297 | See CertExt 298 | """ 299 | 300 | def value(self): 301 | val = b"CA:" + (b"TRUE" if self.ca else b"FALSE") 302 | 303 | if (self.pathLen is not None): 304 | val += b",pathlen:" + str(self.pathLen).encode("utf-8") 305 | 306 | return val 307 | 308 | # Key usage class that represents the RFC5280 4.2.1.3 extension. 309 | 310 | 311 | class KeyUsage(CertExt): 312 | 313 | """ 314 | KeyUsage constructor 315 | :param digitalSignature: assert if digitalSignature 316 | :type digitalSignature: boolean 317 | :param contentCommitment: assert if contentCommitment 318 | :type contentCommitment: boolean 319 | :param keyEncipherment: assert if keyEncipherment 320 | :type keyEncipherment: boolean 321 | :param keyAgreement: assert if keyAgreement 322 | :type keyAgreement: boolean 323 | :param keyCertSign: assert if keyCertSign 324 | :type keyCertSign: boolean 325 | :param cRLSign: assert if cRLSign 326 | :type cRLSign: boolean 327 | :param encipherOnly: assert if encipherOnly 328 | :type encipherOnly: boolean 329 | :param decipherOnly: assert if decipherOnly 330 | :type decipherOnly: boolean 331 | :param critical: assert if critical 332 | :type critical: boolean 333 | :returns: KeyUsage object 334 | """ 335 | 336 | def __init__( 337 | self, digitalSignature=False, contentCommitment=False, 338 | keyEncipherment=False, dataEncipherment=False, keyAgreement=False, 339 | keyCertSign=False, cRLSign=False, encipherOnly=False, 340 | decipherOnly=False, critical=True): 341 | super(self.__class__, self).__init__(critical) 342 | 343 | self.field = {} 344 | self.field['digitalSignature'] = digitalSignature 345 | self.field['nonRepudiation'] = contentCommitment 346 | self.field['keyEncipherment'] = keyEncipherment 347 | self.field['dataEncipherment'] = dataEncipherment 348 | self.field['keyAgreement'] = keyAgreement 349 | self.field['keyCertSign'] = keyCertSign 350 | self.field['cRLSign'] = cRLSign 351 | self.field['encipherOnly'] = encipherOnly 352 | self.field['decipherOnly'] = decipherOnly 353 | 354 | def name(self): 355 | return b"keyUsage" 356 | 357 | def value(self): 358 | val = b"" 359 | for k in self.field.keys(): 360 | if (self.field[k]): 361 | val += (b"" if val == b"" else b",") + k.encode('utf-8') 362 | 363 | return val 364 | 365 | # Extended key usage class that represents the RFC5280 4.2.1.12 extension. 366 | 367 | 368 | class ExtendedKeyUsage(CertExt): 369 | 370 | """ 371 | ExtendedKeyUsage constructor 372 | :param serverAuth: assert if serverAuth 373 | :type serverAuth: boolean 374 | :param clientAuth: assert if clientAuth 375 | :type clientAuth: boolean 376 | :param emailProtection: assert if emailProtection 377 | :type emailProtection: boolean 378 | :param timeStamping: assert if timeStamping 379 | :type timeStamping: boolean 380 | :param OCSPSigning: assert if OCSPSigning 381 | :type OCSPSigning: boolean 382 | :param critical: assert if critical 383 | :type critical: boolean 384 | :returns: ExtendedKeyUsage object 385 | """ 386 | 387 | def __init__(self, serverAuth=False, clientAuth=False, codeSigning=False, 388 | emailProtection=False, timeStamping=False, OCSPSigning=False, 389 | critical=True): 390 | super(self.__class__, self).__init__(critical) 391 | 392 | self.field = {} 393 | self.field['serverAuth'] = serverAuth 394 | self.field['clientAuth'] = clientAuth 395 | self.field['codeSigning'] = codeSigning 396 | self.field['emailProtection'] = emailProtection 397 | self.field['timeStamping'] = timeStamping 398 | self.field['OCSPSigning'] = OCSPSigning 399 | 400 | def name(self): 401 | return b"extendedKeyUsage" 402 | 403 | def value(self): 404 | val = b"" 405 | for k in self.field.keys(): 406 | if (self.field[k]): 407 | val += (b"" if val == b"" else b",") + k.encode('utf-8') 408 | 409 | return val 410 | 411 | # Subject alternative name class that represents the RFC5280 4.2.1.6 extension. 412 | 413 | 414 | class SubjectAltName(CertExt): 415 | 416 | """ 417 | SubjectAltName constructor 418 | :param dnsID: list of DNS-ID 419 | :type dnsID: list of string 420 | :param addrID: list of ADDR-ID 421 | :type addrID: list of string 422 | :param uriID: list of URI-ID 423 | :type uriID: list of string 424 | :param srvID: list of SRV-ID 425 | :type srvID: list of string 426 | :param critical: assert if critical 427 | :type critical: boolean 428 | :returns: SubjectAltName object 429 | """ 430 | 431 | def __init__( 432 | self, dnsID=None, addrID=None, uriID=None, srvID=None, 433 | critical=True): 434 | super(self.__class__, self).__init__(critical) 435 | 436 | self.field = {} 437 | self.field['DNS'] = dnsID if dnsID else [] 438 | self.field['IP'] = addrID if addrID else [] 439 | self.field['URI'] = uriID if uriID else [] 440 | self.field['SRV'] = srvID if srvID else [] 441 | 442 | def name(self): 443 | return b"subjectAltName" 444 | 445 | def value(self): 446 | val = b"" 447 | 448 | for k in self.field.keys(): 449 | ln = b"" 450 | dim = b"" 451 | if (len(self.field[k])): 452 | ln += k.encode('utf-8') + b":" 453 | for v in self.field[k]: 454 | ln += dim + v.encode('utf-8') 455 | dim = b"," 456 | val += (b"" if val == b"" else b",") + ln 457 | 458 | return val 459 | 460 | # Name constraints class that represents the RFC5280 4.2.1.10 extension. 461 | 462 | 463 | class NameConstraints(CertExt): 464 | 465 | """ 466 | NameConstraints constructor 467 | :param permit: list of permitted DNS-ID 468 | :type permit: list of string 469 | :param exclude: list of excluded DNS-ID 470 | :type exclude: list of string 471 | :param critical: assert if critical 472 | :type critical: boolean 473 | :returns: NameConstraints object 474 | """ 475 | 476 | def __init__(self, permit=None, exclude=None, critical=True): 477 | super(self.__class__, self).__init__(critical) 478 | 479 | self.field = {} 480 | self.field['permitted'] = permit if permit else [] 481 | self.field['excluded'] = exclude if exclude else [] 482 | 483 | def name(self): 484 | return b"nameConstraints" 485 | 486 | def value(self): 487 | val = b"" 488 | 489 | for k in self.field.keys(): 490 | ln = b"" 491 | dim = b"" 492 | if (len(self.field[k])): 493 | ln += k.encode('utf-8') + b";DNS:" 494 | for v in self.field[k]: 495 | ln += dim + v.encode('utf-8') 496 | dim = b"," + k.encode('utf-8') + b";DNS:" 497 | val += (b"" if val == b"" else b",") + ln 498 | 499 | return val 500 | 501 | # Certificate modifier class that holds callback functions that alter the behavior 502 | # of the certificate generation logic at different stages 503 | 504 | 505 | class CertMod: 506 | 507 | """ 508 | CertMod constructor 509 | :param hasPreSign: assert if preSign() is overridden 510 | :type hasPreSign: boolean 511 | :param hasPostSign: assert if postSign() is overridden 512 | :type hasPostSign: boolean 513 | :param hasPostWrite: assert if postWrite() is overridden 514 | :type hasPostWrite: boolean 515 | :returns: CertMod object 516 | """ 517 | 518 | def __init__(self, hasPreSign=False, hasPostSign=False, 519 | hasPostWrite=False): 520 | self.hasPreSign = hasPreSign 521 | self.hasPostSign = hasPostSign 522 | self.hasPostWrite = hasPostWrite 523 | 524 | """ 525 | Callback function to be overridden; call immediately before signature process 526 | :param asnObj: certificate to be altered in asn1 format 527 | :type asnObj: pyasn1 object 528 | :returns: pyasn1 object 529 | """ 530 | 531 | def preSign(self, asnObj): 532 | return asnObj 533 | 534 | """ 535 | Callback function to be overridden; call immediately after signature process 536 | :param asnObj: certificate to be altered in asn1 format 537 | :type asnObj: pyasn1 object 538 | :returns: pyasn1 object 539 | """ 540 | 541 | def postSign(self, asnObj): 542 | return asnObj 543 | 544 | """ 545 | Callback function to be overridden; call immediately after write process 546 | :param cert: certificate to be altered 547 | :type cert: Certificate object 548 | :param certPathPrefix: location of the certificate in hard disk 549 | :type certPathPrefix: string 550 | :returns: pyasn1 object 551 | """ 552 | 553 | def postWrite(self, cert, certPathPrefix): 554 | return None 555 | 556 | # Certificate class that represents a X509 certificate 557 | 558 | 559 | class Certificate: 560 | 561 | """ 562 | Certificate constructor 563 | :param certPathPrefix: path prefix of the certificate (destination location) 564 | :type certPathPrefix: string 565 | :param signer: signer information 566 | :type signer: CertSign object 567 | :param security: security information 568 | :type security: CertSec object 569 | :param extensions: certificate extensions 570 | :type extensions: list of CertExt 571 | :param subject: distinguish name 572 | :type subject: CertSubj object 573 | :param modifier: certificate modifiers 574 | :type modifier: CertMod object 575 | :returns: Certificate object 576 | """ 577 | 578 | def __init__( 579 | self, certPathPrefix, signer, security, extensions=None, 580 | subject=None, modifier=None): 581 | 582 | self.certPathPrefix = certPathPrefix 583 | self.signer = signer 584 | self.security = security 585 | self.extensions = extensions if extensions else [] 586 | self.subject = subject if subject else CertSubj(security.fqdn) 587 | self.modifier = modifier if modifier else CertMod() 588 | 589 | """ 590 | Build the certificate and write it to hard disk 591 | :param chainCerts: assert if cert is included in the final PEM file 592 | :type chainCerts: boolean 593 | :returns: pyOpenSSL Certificate object 594 | """ 595 | 596 | def build(self, chainCerts=True): 597 | crtPath = self.certPathPrefix + ".crt" 598 | pemPath = os.path.join( 599 | os.path.dirname( 600 | self.certPathPrefix), 601 | DEFAULT_PEM_NAME) 602 | 603 | cert = crypto.X509() 604 | cert.set_version(self.security.version) 605 | cert.set_serial_number(self.security.serial) 606 | cert.gmtime_adj_notBefore(self.security.notBefore * 3600) 607 | cert.gmtime_adj_notAfter(self.security.notAfter * 3600) 608 | cert.set_subject(self.subject.getSubject()) 609 | cert.set_issuer(self.signer.signSubj) 610 | cert.set_pubkey(self.security.getKey()) 611 | 612 | extList = [] 613 | for ext in self.extensions: 614 | extList.append( 615 | crypto.X509ExtensionType( 616 | ext.name(), 617 | ext.criticality(), 618 | ext.value())) 619 | cert.add_extensions(extList) 620 | 621 | if (self.modifier.hasPreSign): 622 | cert = self.asnModify(cert, self.modifier.preSign) 623 | cert.sign(self.signer.getKey(), self.security.digest) 624 | if (self.modifier.hasPostSign): 625 | cert = self.asnModify(cert, self.modifier.postSign) 626 | 627 | with open(crtPath, 'wb+') as f: 628 | f.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert)) 629 | if (self.modifier.hasPostWrite): 630 | self.modifier.postWrite(self.modifier, self.certPathPrefix) 631 | 632 | if (chainCerts): 633 | if (not os.path.exists(pemPath)): 634 | with open(pemPath, 'wb+') as f: 635 | if (self.signer.signPathPrefix): 636 | rootPem = self.signer.signPathPrefix + ".pem" 637 | if (os.path.exists(rootPem)): 638 | with open(rootPem, "rb") as r: 639 | f.write(r.read()) 640 | concatFiles(crtPath, pemPath, pemPath) 641 | 642 | self.cert = cert 643 | return cert 644 | 645 | """ 646 | Get this certificate 647 | :returns: pyOpenSSL Certificate object 648 | """ 649 | 650 | def getCert(self): 651 | return self.cert 652 | 653 | """ 654 | Indicate a self-signed certificate 655 | """ 656 | def selfSign(self): 657 | self.security.certKey.build() 658 | self.signer = CertSign( 659 | None, 660 | self.security.certKey, 661 | self.subject.getSubject()) 662 | 663 | """ 664 | Add an extension entry to this certificate 665 | :param extension: extension to be added to the certificate 666 | :type extension: CertExt object 667 | :returns: CertExt object 668 | """ 669 | 670 | def addExtension(self, extension): 671 | if (self.getExtension(extension.__class__)): 672 | raise Exception( 673 | "The extension type %s already exists" % 674 | extension.__class__.__name__) 675 | 676 | self.extensions.append(extension) 677 | 678 | return extension 679 | 680 | """ 681 | Get an extension entry, with specified type, from this certificate 682 | :param extendType: extension type 683 | :type extendType: CertExt Class object 684 | :returns: CertExt object; None if no matching type if found 685 | """ 686 | 687 | def getExtension(self, extendType): 688 | for e in self.extensions: 689 | if (e.__class__ == extendType): 690 | return e 691 | 692 | return None 693 | 694 | """ 695 | Remove an extension entry, with specified type, from this certificate 696 | :param extendType: extension type 697 | :type extendType: CertExt Class object 698 | :returns: CertExt object; None if no matching type if found 699 | """ 700 | 701 | def removeExtension(self, extendType): 702 | rtn = None 703 | i = 0 704 | 705 | for e in self.extensions: 706 | if (e.__class__ == extendType): 707 | rtn = e 708 | del self.extensions[i] 709 | break 710 | i += 1 711 | 712 | return rtn 713 | 714 | """ 715 | Writes the key pairs used in this certificate to file 716 | :param path: destination of the file in the file system 717 | :type path: string 718 | :param keyPassword: pass phrase for the file 719 | :type keyPassword: string 720 | :returns: string 721 | """ 722 | 723 | def writeKey(self, path=None, keyPassword=None): 724 | keyPath = path if path else self.certPathPrefix + ".key" 725 | 726 | with open(keyPath, 'wb+') as f: 727 | if (keyPassword): 728 | f.write( 729 | crypto.dump_privatekey( 730 | crypto.FILETYPE_PEM, 731 | self.security.getKey(), 732 | DEFAULT_CIPHER, 733 | keyPassword.encode('utf-8'))) 734 | 735 | else: 736 | f.write( 737 | crypto.dump_privatekey( 738 | crypto.FILETYPE_PEM, 739 | self.security.getKey())) 740 | 741 | return keyPath 742 | 743 | """ 744 | Transform a x509 object to asn1 object, call the callback function 745 | to with the asn1 object, and transform the result back to x509 746 | :param cert: x509 certificate 747 | :type cert: pyOpenSSL Certificate object 748 | :param func: callback function that transform the certificate 749 | :type func: Function object 750 | :returns: pyOpenSSL Certificate object 751 | """ 752 | 753 | def asnModify(self, cert, func): 754 | substrate = crypto.dump_certificate(crypto.FILETYPE_ASN1, cert) 755 | cert = decoder.decode(substrate, asn1Spec=rfc2459.Certificate())[0] 756 | 757 | cert = func(cert) 758 | 759 | substrate = encoder.encode(cert) 760 | return crypto.load_certificate(crypto.FILETYPE_ASN1, substrate) 761 | -------------------------------------------------------------------------------- /src/Definitions.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | This file contains all macro definitions, utility functions, and libraries 4 | used in this software. Any changes of value made in this file will propagate 5 | to all test cases generated by this software. Some settings that are not 6 | possible to configure in command line may be easily specified in this file. 7 | 8 | @author: Calvin Jia Liang 9 | Created on Sep 3, 2014 10 | """ 11 | 12 | import os 13 | import ssl 14 | import shutil 15 | import sys 16 | import socket 17 | import select 18 | import time 19 | import copy 20 | import subprocess 21 | import math 22 | import queue 23 | from OpenSSL import * 24 | 25 | # current version of this software; should be updated for each modification 26 | SOFTWARE_VERSION = "0.2.3" 27 | 28 | # current version of test case; should be updated for each modification 29 | TEST_VERSION = "1.1" 30 | 31 | # default subject field entries other than the CN 32 | DEFAULT_C = "US" 33 | DEFAULT_ST = "CA" 34 | DEFAULT_L = "San Luis Obispo" 35 | DEFAULT_O = "Certificate Validation and Compliance" 36 | DEFAULT_U = "X509 Verification Checker" 37 | DEFAULT_EMAIL = "someone@mail.com" 38 | 39 | # default x509 settings 40 | DEFAULT_KSIZE = 1024 41 | DEFAULT_KTYPE = crypto.TYPE_RSA 42 | DEFAULT_HOUR_BEFORE = -48 43 | DEFAULT_HOUR_AFTER = 8760 44 | DEFAULT_VERSION = 0x02 45 | DEFAULT_DIGEST = "SHA1" 46 | DEFAULT_SUITE = "DEFAULT" 47 | 48 | # default certificate and key file operation setting 49 | DEFAULT_PASSWORD = "test" 50 | DEFAULT_CIPHER = "aes-256-cbc" 51 | DEFAULT_PEM_NAME = "cert.pem" 52 | DEFAULT_KEY_NAME = "cert.key" 53 | 54 | # default file paths 55 | DEFAULT_SERIAL_PATH = os.path.join(".", "serial") 56 | DEFAULT_LICENSE_PATH = os.path.join(".", "LICENSE") 57 | DEFAULT_CA_PREFIX = os.path.join(".", "ca", "ca") 58 | DEFAULT_CERT_DIR = os.path.join(".", "certs", "") 59 | 60 | # other static settings throughout the test 61 | REPEAT = 3 62 | HOUR_DISCREPANCY = 48 63 | INVALID_TRAIL = ".invalid.test" 64 | NONSTANDARD_OID = "1.3.6.1.4.1.11129.2.5.1" 65 | 66 | # other default settings throughout the test 67 | DEFAULT_NUM_CHAINED = 4 68 | DEFAULT_SERIAL = 1001 69 | DEFAULT_PAUSE = 0 70 | DEFAULT_CA_NAME = "verify.x509.test" 71 | DEFAULT_METADATA_NAME = "metadata" 72 | 73 | # rank macros for severity 74 | SEV_HIGH = "High" 75 | # invalid CA certificate becomes valid 76 | SEV_MED = "Medium" 77 | # invalid leaf certificate becomes valid 78 | SEV_LOW = "Low" 79 | # compliance issue 80 | 81 | # rank macros for ease of execution 82 | EASE_HIGH = "High" 83 | # require no CA or compliant CA with basic server sign 84 | EASE_MED = "Medium" 85 | # require non-compliant CA with server sign 86 | EASE_LOW = "Low" 87 | # require CA with CA sign or compromise of other information 88 | 89 | # default server settings 90 | DEFAULT_PORT = 443 91 | DEFAULT_SSL_VER = SSL.SSLv23_METHOD 92 | DEFAULT_ADDR = "127.0.0.1" 93 | 94 | # functionality test set 95 | FUNC_KEY_SIZES = [512, 1024, 2048, 1025] 96 | FUNC_KEY_TYPES = [crypto.TYPE_RSA] 97 | FUNC_HASH_TYPES = ["MD5"] 98 | FUNC_CIPHER_SUITES = ["DEFAULT", "HIGH", "MEDIUM", 99 | "LOW", "aNULL", "eNULL"] 100 | FUNC_SSL_VERSIONS = [SSL.TLSv1_2_METHOD, SSL.TLSv1_1_METHOD, 101 | SSL.TLSv1_METHOD, SSL.SSLv3_METHOD] 102 | 103 | # overflow test set 104 | OVERFLOW_VALID_CA = True 105 | OVERFLOW_CHAIN_LEN = 100 106 | OVERFLOW_EXT_LEN = 500 107 | OVERFLOW_OID_MUL = 500 108 | DEFAULT_OVERFLOW_LENGTH = 640000 109 | 110 | # debugging options 111 | VERBOSE = False 112 | 113 | # global variables 114 | serial = None 115 | 116 | 117 | # utility functions 118 | 119 | """ 120 | Get the default fqdn of the intermediate CA 121 | :param lev: level of depth below the root CA 122 | :type lev: integer 123 | :returns: string 124 | """ 125 | 126 | 127 | def getIntCAName(lev): 128 | return "int.lev%i%s" % (lev, getIntCADomain()) 129 | 130 | """ 131 | Get the default domain name of the intermediate CA 132 | :returns: string 133 | """ 134 | 135 | 136 | def getIntCADomain(): 137 | return ".ca.authority" 138 | 139 | """ 140 | Get an invalid fqdn from the valid fqdn 141 | :param testName: valid fqdn 142 | :type testName: string 143 | :returns: string 144 | """ 145 | 146 | 147 | def getInvalidDomain(testName): 148 | return testName + INVALID_TRAIL 149 | 150 | """ 151 | Get an invalid null extended fqdn from the valid fqdn 152 | :param testName: valid fqdn 153 | :type testName: string 154 | :returns: string 155 | """ 156 | 157 | 158 | def getInvalidNullDomain(testName): 159 | return testName + "\0" + INVALID_TRAIL 160 | 161 | """ 162 | Get a new test name for the chain extended test case 163 | :param testName: original test name 164 | :type testName: string 165 | :returns: string 166 | """ 167 | 168 | 169 | def getChainedName(testName): 170 | return testName + "Chained" 171 | 172 | """ 173 | Get a new test name for the altname extended test case 174 | :param testName: original test name 175 | :type testName: string 176 | :returns: string 177 | """ 178 | 179 | 180 | def getAltExtendedName(testName): 181 | return testName + "AltName" 182 | 183 | 184 | def isIPAddr(fqdn): 185 | ip = fqdn.split('.') 186 | 187 | if (len(ip) != 4): 188 | return False 189 | 190 | for a in ip: 191 | try: 192 | o = int(a) 193 | except: 194 | return False 195 | 196 | if (o < 0 or o > 255): 197 | return False 198 | 199 | return True 200 | 201 | """ 202 | Get a new serial number that is an increment of the last number; first number 203 | obtained from the serial file 204 | :note: this function modifies global variable 205 | :param filePath: path to the serial file 206 | :type filePath: string 207 | :returns: integer 208 | """ 209 | 210 | 211 | def getNewSerial(filePath=DEFAULT_SERIAL_PATH): 212 | global serial 213 | 214 | if (not serial): 215 | with open(filePath, 'r') as f: 216 | serial = int(f.readline().strip()) 217 | 218 | rtn = serial 219 | serial += 1 220 | return rtn 221 | 222 | """ 223 | Save the serial number to file 224 | :param filePath: path to the serial file 225 | :type filePath: string 226 | """ 227 | 228 | 229 | def saveSerial(filePath=DEFAULT_SERIAL_PATH): 230 | with open(filePath, 'w') as f: 231 | f.write(str(serial)) 232 | 233 | 234 | def unmarkCriticalExtensions(case): 235 | cnt = 0 236 | 237 | for cert in case.certs: 238 | for extension in cert.extensions: 239 | if (extension.criticality()): 240 | extension.critical = False 241 | cnt += 1 242 | 243 | return cnt 244 | 245 | """ 246 | Get the license of this software 247 | :param filePath: path to the license file 248 | :type filePath: string 249 | :return: string 250 | """ 251 | 252 | 253 | def getLicense(path=DEFAULT_LICENSE_PATH): 254 | data = "" 255 | 256 | try: 257 | with open(path, 'r') as f: 258 | data = f.read() 259 | except: 260 | pass 261 | # raise Exception("Missing license file"); 262 | 263 | return data 264 | 265 | """ 266 | Concatenate two files (first + second) together to form a new file 267 | :param src: path to the first file 268 | :type src: string 269 | :param oth: path to the second file 270 | :type oth: string 271 | :param dest: path to the destination file 272 | :type dest: string 273 | """ 274 | 275 | 276 | def concatFiles(src, oth, dest): 277 | with open(src, 'rb') as s: 278 | with open(oth, 'rb') as o: 279 | data = s.read() + o.read() 280 | 281 | with open(dest, 'wb+') as d: 282 | d.write(data) 283 | 284 | """ 285 | Prints a message then terminate the program immediately 286 | :param msg: message to print out 287 | :type msg: string 288 | :param log: output stream 289 | :type log: Function object 290 | """ 291 | 292 | 293 | def forcedExit(msg, log): 294 | log(msg) 295 | log("Program Exits with Error.") 296 | sys.exit(1) 297 | -------------------------------------------------------------------------------- /src/Terminal.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file holds the Terminal class that parse user input and process 3 | system output. It mainly parse the options given by the user from the 4 | runtime argument to a internal data structure. 5 | 6 | @author: Calvin Jia Liang 7 | Created on Sep 3, 2014 8 | """ 9 | 10 | from src.TestServer import * 11 | from optparse import OptionParser 12 | 13 | # Terminal class that handles all terminal I/O. 14 | 15 | 16 | class Terminal: 17 | 18 | def __init__(self): 19 | pass 20 | 21 | def usage(self): 22 | return "usage: " + sys.argv[0] + " fqdn [options]" 23 | 24 | """ 25 | Build the option attributes from user arguments 26 | :returns: string, parser Option object 27 | """ 28 | 29 | def build(self): 30 | parser = OptionParser(usage=self.usage()) 31 | # parser.add_option("-n", "--fqdn", dest="fqdn", default=None, \ 32 | # help="fully-qualified-domain-name of your domain"); 33 | parser.add_option("-a", "--address", dest="addr", 34 | default=DEFAULT_ADDR, 35 | help="IP address of your domain") 36 | parser.add_option("", "--service", dest="serv", 37 | help="service offered by your domain") 38 | parser.add_option("", "--ssl", dest="sver", 39 | help="ssl/tls version; possible values are SSLv2," + 40 | " SSLv3, [SSLv23], TLSv1_0, TLSv1_1, TLSv1_2") 41 | parser.add_option("", "--exclude-all", dest="exclude", 42 | help="exclude all specified test groups/cases;" + 43 | " comma separated name-list without spaces") 44 | parser.add_option("", "--include-only", dest="include", 45 | help="include only specified test groups/cases;" + 46 | " comma separated name-list without spaces") 47 | parser.add_option("-c", "--component", dest="comp", 48 | help="specify components to run;" + 49 | " possible values are full, func, [cert], overflow") 50 | 51 | parser.add_option("-p", "--port", dest="port", 52 | type="int", default=DEFAULT_PORT, 53 | help="port number of the server") 54 | parser.add_option("", "--key-length", dest="kSize", 55 | type="int", default=DEFAULT_KSIZE, 56 | help="key length for all certificates") 57 | parser.add_option("", "--overflow-length", dest="overflowLen", 58 | type="int", default=DEFAULT_OVERFLOW_LENGTH, 59 | help="byte length of the overflow filler") 60 | parser.add_option("", "--pause", dest="pause", 61 | type="int", default=DEFAULT_PAUSE, 62 | help="number of seconds between each test case") 63 | 64 | parser.add_option("-r", "--replace", action="store_true", 65 | dest="replace", default=False, 66 | help="rebuild all test certificates (NOTE: this" + 67 | " option will REMOVE ALL ITEMS under path" + 68 | " configured by --test-dir)") 69 | parser.add_option("", "--all", action="store_true", 70 | dest="all", default=False, 71 | help="include all test cases; " + 72 | "disregard results from compatibility test") 73 | parser.add_option("", "--diligent", action="store_true", 74 | dest="diligent", default=False, 75 | help="every test case is repeated three times " + 76 | "followed by a valid base case") 77 | parser.add_option("-q", "--quiet", action="store_true", 78 | dest="quiet", default=False, 79 | help="show only failed test cases") 80 | parser.add_option("-l", "--list", action="store_true", 81 | dest="list", default=False, 82 | help="list all test cases then exit") 83 | parser.add_option("", "--cert-only", action="store_true", 84 | dest="conly", default=False, 85 | help="generate all test cases then exit") 86 | parser.add_option("", "--version", action="store_true", 87 | dest="pver", default=False, 88 | help="print version information then exit") 89 | 90 | parser.add_option("", "--ca-prefix", dest="caPathPrefix", 91 | default=DEFAULT_CA_PREFIX, 92 | help="set root ca path prefix (ie. /certs/ca)") 93 | parser.add_option("", "--test-dir", dest="testDir", 94 | default=DEFAULT_CERT_DIR, 95 | help="set directory path for test cases (NOTE: " + 96 | "setting this path incorrectly may result in lost " + 97 | "of data; thus, it is recommended to not use this " + 98 | "option if possible)") 99 | parser.add_option("", "--ca-password", dest="caPassword", 100 | default=DEFAULT_PASSWORD, 101 | help="specify password for CA private key") 102 | parser.add_option("-w", "--write", dest="logPath", 103 | default=None, help="set path for output file") 104 | 105 | (opt, args) = parser.parse_args(sys.argv) 106 | 107 | self.logStream = self.getLogStream(opt.logPath) 108 | self.addr = opt.addr 109 | self.port = opt.port 110 | self.kSize = opt.kSize 111 | self.overflowLen = opt.overflowLen 112 | self.pause = opt.pause 113 | self.serv = opt.serv 114 | self.sslVer = self.getSSLVer(opt.sver) 115 | self.exclude = self.getExclude(opt.exclude, opt.include) 116 | self.replace = opt.replace and not opt.list 117 | self.all = opt.all 118 | self.diligent = opt.diligent 119 | self.quiet = opt.quiet 120 | self.list = opt.list 121 | self.caPathPrefix = opt.caPathPrefix 122 | self.testDir = opt.testDir 123 | self.caPassword = opt.caPassword 124 | self.compCert, self.compFunc, self.compOverflow = self.getComp(opt.comp) 125 | self.conly = opt.conly 126 | self.pver = opt.pver 127 | 128 | if (len(args) != 2): 129 | forcedExit(self.usage(), self.log) 130 | return args[1], opt 131 | 132 | def getExclude(self, exclude, include): 133 | ls = {} 134 | 135 | if (include): 136 | ls = TestSet.getAllNames(include.split(',')) 137 | if (exclude): 138 | for e in exclude.split(','): 139 | ls[e] = e 140 | 141 | return ls 142 | 143 | def getSSLVer(self, sver): 144 | ver = None 145 | 146 | if (sver is None): 147 | ver = SSL.SSLv23_METHOD 148 | elif (sver == "SSLv2"): 149 | ver = SSL.SSLv2_METHOD 150 | elif (sver == "SSLv3"): 151 | ver = SSL.SSLv3_METHOD 152 | elif (sver == "SSLv23"): 153 | ver = SSL.SSLv23_METHOD 154 | elif (sver == "TLSv1_0"): 155 | ver = SSL.TLSv1_METHOD 156 | elif (sver == "TLSv1_1"): 157 | ver = SSL.TLSv1_1_METHOD 158 | elif (sver == "TLSv1_2"): 159 | ver = SSL.TLSv1_2_METHOD 160 | else: 161 | forcedExit("Unknown SSL/TLS Version.", self.log) 162 | 163 | return ver 164 | 165 | def getLogStream(self, path): 166 | stream = sys.stdout 167 | 168 | if (path): 169 | if (path == '-'): 170 | pass 171 | elif (path == '+'): 172 | stream = sys.stderr 173 | else: 174 | stream = open(path, "w+") 175 | return stream 176 | 177 | def getComp(self, comp): 178 | compCert = compFunc = compOverflow = False 179 | 180 | if (not comp or comp == "cert"): 181 | compCert = True 182 | elif (comp == "func"): 183 | compFunc = True 184 | elif (comp == "overflow"): 185 | compOverflow = True 186 | elif (comp == "full"): 187 | compCert = compFunc = compOverflow = True 188 | else: 189 | raise Exception("Invalid selection value: " + comp) 190 | 191 | return compCert, compFunc, compOverflow 192 | 193 | """ 194 | Output stream callback function 195 | :param msg: message to output 196 | :type msg: string 197 | :param delim: delimiter at the end of the string 198 | :type delim: string 199 | """ 200 | 201 | def log(self, msg, delim='\n'): 202 | self.logStream.write(str(msg) + delim) 203 | 204 | def showProgress(self, part, whole): 205 | check = int(whole / 20) 206 | if (part % check == 0): 207 | self.log('.', '') 208 | if (part == whole): 209 | self.log('') 210 | 211 | def printVersion(self): 212 | self.log("") 213 | self.log(getLicense()) 214 | 215 | self.log("") 216 | self.log("Software Version %s" % (SOFTWARE_VERSION)) 217 | self.log("Test Case Version %s" % (TEST_VERSION)) 218 | 219 | """ 220 | Calling lower level functions to parse user arguments, build test cases, 221 | and execute the tests through the server. 222 | """ 223 | 224 | def runTest(self): 225 | fqdn, _ = self.build() 226 | 227 | self.log("Starting SSL/TLS X509 Certificate Test") 228 | complete = True 229 | cases = TestSet(fqdn, self) 230 | 231 | if (self.list): 232 | self.log("\nLegends:") 233 | self.log("(>) means test group or branch node") 234 | self.log("(*) means critical test case") 235 | self.log("(+) means extended test case") 236 | self.log("(-) means normal test case") 237 | self.log("\nTest Cases:") 238 | cnt = cases.printAllTestCases(TestCase, "") 239 | self.log("Total of " + str(cnt) + " test case(s).") 240 | elif (self.conly): 241 | cases.build() 242 | self.log("Done") 243 | elif (self.pver): 244 | self.printVersion() 245 | else: 246 | cases = cases.build() 247 | test = TestServer(cases.getTestSet(), cases.getBaseCase(), self) 248 | complete = test.run() 249 | 250 | self.log('') 251 | self.log("Program Exits" + (" with Errors" if not complete else 252 | " Correctly")) 253 | self.logStream.close() 254 | -------------------------------------------------------------------------------- /src/Test.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file contains the root (abstract) class and its peripheral classes. 3 | Class TestCase is the root node in the test case inheritance hierarchy. 4 | Class TestCase provides the core functionality for all test cases. 5 | The peripheral classes supply information to the TestCase object. 6 | 7 | @author: Calvin Jia Liang 8 | Created on May 15, 2014 9 | """ 10 | 11 | from src.Certificate import * 12 | 13 | # Metadata class that describes a test case and controls the behavior of the 14 | # corresponding connection. 15 | 16 | 17 | class TestMetadata: 18 | 19 | """ 20 | TestMetadata constructor 21 | :param name: name of the test case 22 | :type name: string 23 | :param ref: authentic reference that describe the correct behavior 24 | :type ref: string 25 | :param severity: severity if the test case failed 26 | :type severity: Definition macro 27 | :param ease: ease of execution 28 | :type ease: Definition macro 29 | :param isCritical: assert if dependency check 30 | :type isCritical: boolean 31 | :param isValid: assert if valid or positive test case 32 | :type isValid: boolean 33 | :param chainable: assert if test case can be chained 34 | :type chainable: boolean 35 | :param altextend: assert if test case can be altname extended 36 | :type altextend: boolean 37 | :param functional: assert if functionality test case (inherently positive) 38 | :type functional: boolean 39 | :param overflow: assert if overflow test case (inherently negative) 40 | :type overflow: boolean 41 | :param sslVer: the SSL/TLS handshake used in this connection 42 | :type sslVer: pyOpenSSL macro 43 | :param caPathPrefix: the path prefix of the root CA 44 | :type caPathPrefix: string 45 | :param testDir: the top level test directory path 46 | :type testDir: string 47 | :param suite: the acceptable crypto suite used in this connection 48 | :type suite: string 49 | :returns: TestMetadata object 50 | """ 51 | 52 | def __init__(self, name, ref, severity, ease, isCritical=False, 53 | isValid=False, chainable=False, altextend=False, 54 | functional=False, overflow=False, sslVer=None, 55 | caPathPrefix=DEFAULT_CA_PREFIX, testDir=DEFAULT_CERT_DIR, 56 | suite=DEFAULT_SUITE): 57 | 58 | self.name = name 59 | self.ref = ref 60 | self.severity = severity 61 | self.ease = ease 62 | self.isCritical = isCritical 63 | self.isValid = isValid 64 | 65 | self.suite = suite 66 | self.sslVer = sslVer 67 | 68 | self.chainable = chainable 69 | self.altextend = altextend 70 | self.functional = functional 71 | self.overflow = overflow 72 | 73 | self.caPathPrefix = caPathPrefix 74 | self.testDir = testDir 75 | 76 | """ 77 | Write all metadata attributes to file 78 | :param path: path of the metadata file 79 | :type path: string 80 | """ 81 | 82 | def write(self, path=None): 83 | if (not path): 84 | path = os.path.join(self.testDir, self.name, DEFAULT_METADATA_NAME) 85 | 86 | with open(path, 'w+') as f: 87 | f.write("%s=%s\n" % ("name", self.name)) 88 | f.write("%s=%s\n" % ("ref", self.ref)) 89 | f.write("%s=%s\n" % ("severity", self.severity)) 90 | f.write("%s=%s\n" % ("ease", self.ease)) 91 | f.write("%s=%s\n" % ("isCritical", self.isCritical)) 92 | f.write("%s=%s\n" % ("isValid", self.isValid)) 93 | f.write("%s=%s\n" % ("suite", self.suite)) 94 | f.write("%s=%s\n" % ("sslVer", self.sslVer)) 95 | f.write("%s=%s\n" % ("chainable", self.chainable)) 96 | f.write("%s=%s\n" % ("altextend", self.altextend)) 97 | f.write("%s=%s\n" % ("functional", self.functional)) 98 | f.write("%s=%s\n" % ("overflow", self.overflow)) 99 | f.write("%s=%s\n" % ("caPathPrefix", self.caPathPrefix)) 100 | f.write("%s=%s\n" % ("testDir", self.testDir)) 101 | 102 | def load(self, path=None): 103 | pass 104 | 105 | # Information class that provides user and system setting information 106 | # to all test cases. 107 | 108 | 109 | class Information: 110 | 111 | """ 112 | Information constructor 113 | :param log: output stream callback function 114 | :type log: Function object 115 | :param caPathPrefix: the path prefix of the signer 116 | :type caPathPrefix: string 117 | :param testDir: the top level test directory path 118 | :type testDir: string 119 | :param caPassword: password for the signer's key file 120 | :type caPassword: string 121 | :param addr: IP address of the connection 122 | :type addr: string 123 | :param port: port number of the connection 124 | :type port: integer 125 | :param kSize: length of the key 126 | :type kSize: integer 127 | :param metadata: substitute Metadata object (used in Expander only) 128 | :type metadata: Metadata object 129 | :returns: Information object 130 | """ 131 | 132 | def __init__(self, log, caPathPrefix=DEFAULT_CA_PREFIX, 133 | testDir=DEFAULT_CERT_DIR, caPassword=None, addr=None, 134 | port=None, kSize=None, metadata=None): 135 | 136 | self.caPathPrefix = caPathPrefix 137 | self.testDir = testDir 138 | self.caPassword = caPassword 139 | 140 | self.addr = addr 141 | self.port = port 142 | self.kSize = kSize 143 | 144 | self.metadata = metadata 145 | self.log = log 146 | 147 | # Test case class that represents an individual test case. 148 | 149 | 150 | class TestCase: 151 | 152 | """ 153 | TestCase constructor 154 | :param fqdn: fully quantifiable domain name of the test case 155 | :type fqdn: string 156 | :param metadata: metadata accompanies the test case 157 | :type metadata: TestMetadata object 158 | :param info: other information for the test session 159 | :type info: Information object 160 | :param depth: number of certificates chained (including the leaf) 161 | :type depth: integer 162 | :returns: TestCase object 163 | """ 164 | 165 | def __init__(self, fqdn, metadata, info, depth=1): 166 | self.newTestCase(fqdn, metadata, info, depth) 167 | 168 | def newTestCase(self, fqdn, metadata, info, depth): 169 | self.fqdn = fqdn 170 | self.metadata = info.metadata if info.metadata else metadata 171 | self.info = info 172 | self.depth = depth 173 | 174 | if (info.caPathPrefix): 175 | self.metadata.caPathPrefix = info.caPathPrefix 176 | if (info.testDir): 177 | self.metadata.testDir = info.testDir 178 | 179 | self.certs = [] 180 | pKey = pSubj = None 181 | i = 1 182 | while (depth > 0): 183 | extensions = [] 184 | 185 | if (depth > 1): 186 | certName = getIntCAName(i) 187 | certPathPrefix = os.path.join(self.getCertDir(), certName) 188 | extensions.append(BasicConstraint(True)) 189 | extensions.append(KeyUsage(keyCertSign=True, cRLSign=True)) 190 | else: 191 | certName = fqdn 192 | certPathPrefix = os.path.join(self.getCertDir(), 193 | self.metadata.name) 194 | 195 | if (self.getDepth() == 0): 196 | signer = CertSign(self.getCAPathPrefix(), 197 | keyPassword=self.info.caPassword) 198 | else: 199 | signer = CertSign(None, pKey, pSubj) 200 | 201 | if (info.kSize): 202 | security = CertSec(certName, kSize=info.kSize) 203 | else: 204 | security = CertSec(certName) 205 | 206 | cert = Certificate(certPathPrefix, signer, security, extensions) 207 | pKey = cert.security.certKey 208 | pSubj = cert.subject.getSubject() 209 | self.certs.append(cert) 210 | 211 | depth -= 1 212 | i += 1 213 | 214 | return self 215 | 216 | """ 217 | Test preparation that make sure the destination folder is ready 218 | :param replace: assert if erasing all previous test case files 219 | :type replace: boolean 220 | :returns: boolean 221 | """ 222 | 223 | def testPrep(self, replace=False): 224 | if (replace and os.path.exists(self.getCertDir())): 225 | shutil.rmtree(self.getCertDir()) 226 | if (not os.path.exists(self.getCertDir())): 227 | os.mkdir(self.getCertDir()) 228 | replace = True 229 | return replace 230 | 231 | """ 232 | Build test if necessary 233 | :param replace: assert if erasing previous test case files 234 | :type replace: boolean 235 | """ 236 | 237 | def testBuild(self, replace=False): 238 | if (self.testPrep(replace)): 239 | self.procedure() 240 | 241 | """ 242 | Build all certificates in the test case by calling corresponding build() 243 | methods and write the key file of the last certificate to hard disk 244 | """ 245 | 246 | def procedure(self): 247 | lastCert = None 248 | i = 1 249 | 250 | for cert in self.certs: 251 | cert.security.build() 252 | cert.signer.build() 253 | cert = self.preCertBuild(cert, i) 254 | cert.build() 255 | lastCert = cert 256 | i += 1 257 | 258 | lastCert.writeKey(self.getKeyPath()) 259 | self.metadata.write() 260 | 261 | """ 262 | Function to override if the test case requires certain procedure to be 263 | done immediately before calling build() of the certificate 264 | :param cert: the certificate in question 265 | :type cert: Certificate object 266 | :param idx: index of the CA in the chain 267 | :type idx: integer 268 | :returns: Certificate object 269 | """ 270 | 271 | def preCertBuild(self, cert, idx): 272 | return cert 273 | 274 | """ 275 | Replace the specified CA with another certificate; BasicConstraint and 276 | KeyUsage extensions will be correctly added to form a valid CA 277 | :param idx: index of the CA in the chain to be replaced 278 | :type idx: integer 279 | :param cert: the successor certificate 280 | :type cert: Certificate object 281 | """ 282 | 283 | def replaceCA(self, idx, cert): 284 | cert.certPathPrefix = os.path.join(self.getCertDir(), 285 | self.certs[idx].security.fqdn) 286 | cert.addExtension(BasicConstraint(True)) 287 | cert.addExtension(KeyUsage(keyCertSign=True, cRLSign=True)) 288 | 289 | self.certs[idx] = cert 290 | self.certs[idx + 1].signer.signKey = cert.security.certKey 291 | 292 | """ 293 | Add AltName extensions to all certificates in the test case; the new 294 | altname follows the DNS-ID of the fqdn 295 | :param critical: assert if critical extension 296 | :type critical: boolean 297 | """ 298 | 299 | def includeAltName(self, critical=True): 300 | for cert in self.certs: 301 | try: 302 | cert.addExtension(SubjectAltName(critical=critical)) 303 | except: 304 | pass 305 | cert.getExtension(SubjectAltName).field['DNS'].\ 306 | append(cert.security.fqdn) 307 | 308 | """ 309 | Get the number of certificates chained in this test case 310 | (excluding the root CA) 311 | :returns: integer 312 | """ 313 | 314 | def getDepth(self): 315 | return len(self.certs) 316 | 317 | """ 318 | Get the first CA (the one immediately signed by the root CA) 319 | :returns: Certificate object 320 | """ 321 | 322 | def getFirstCA(self): 323 | return self.certs[0] if self.getDepth() > 1 else None 324 | 325 | """ 326 | Get the second CA (the one immediately signed by the first CA) 327 | :returns: Certificate object 328 | """ 329 | 330 | def getSecondCA(self): 331 | return self.certs[1] if self.getDepth() > 2 else None 332 | 333 | """ 334 | Get the leaf CA (the one that directly signs the server certificate) 335 | :returns: Certificate object 336 | """ 337 | 338 | def getEdgeCA(self): 339 | return self.certs[-2] if self.getDepth() > 2 else None 340 | 341 | """ 342 | Get the server certificate (the one used in SSL/TLS key agreement) 343 | :returns: Certificate object 344 | """ 345 | 346 | def getServCert(self): 347 | return self.certs[-1] 348 | 349 | """ 350 | Get the cipher suite used in this test case 351 | :returns: string 352 | """ 353 | 354 | def getCipherSuite(self): 355 | return self.metadata.suite 356 | 357 | """ 358 | Get the SSL/TLS version used in this test case 359 | :returns: pyOpenSSL macro 360 | """ 361 | 362 | def getSSLVersion(self): 363 | return self.metadata.sslVer 364 | 365 | """ 366 | Get if the test can form a new chained test case 367 | :returns: boolean 368 | """ 369 | 370 | def isChainable(self): 371 | return self.metadata.chainable 372 | 373 | """ 374 | Get if the test can form a new altname test case 375 | :returns: boolean 376 | """ 377 | 378 | def isAltExtend(self): 379 | return self.metadata.altextend 380 | 381 | """ 382 | Get if the test case checks functionality instead of certificate validation 383 | :returns: boolean 384 | """ 385 | 386 | def isFunctional(self): 387 | return self.metadata.functional 388 | 389 | """ 390 | Get if the test case checks overflow instead of certificate validation 391 | :returns: boolean 392 | """ 393 | 394 | def isOverflow(self): 395 | return self.metadata.overflow 396 | 397 | """ 398 | Get the directory path that holds the test files 399 | :returns: string 400 | """ 401 | 402 | def getCertDir(self): 403 | return os.path.join(self.metadata.testDir, self.metadata.name, "") 404 | 405 | """ 406 | Get the file path of the final PEM file to send 407 | :returns: string 408 | """ 409 | 410 | def getPemPath(self): 411 | return os.path.join(self.getCertDir(), DEFAULT_PEM_NAME) 412 | 413 | """ 414 | Get the file path of the server certificate keys for the server 415 | :returns: string 416 | """ 417 | 418 | def getKeyPath(self): 419 | return os.path.join(self.getCertDir(), DEFAULT_KEY_NAME) 420 | 421 | """ 422 | Get the path prefix of the root CA 423 | :returns: string 424 | """ 425 | 426 | def getCAPathPrefix(self): 427 | return self.metadata.caPathPrefix 428 | 429 | """ 430 | Get if the test case is a positive test (a test that is valid) 431 | :returns: boolean 432 | """ 433 | 434 | def getTestType(self): 435 | return self.metadata.isValid 436 | 437 | """ 438 | Get if the test case has feature support implication when failed 439 | :returns: boolean 440 | """ 441 | 442 | def getCritical(self): 443 | return self.metadata.isCritical 444 | 445 | """ 446 | Get the authentic reference that describe the correct behavior 447 | :returns: string 448 | """ 449 | 450 | def getReference(self): 451 | return self.metadata.ref 452 | 453 | """ 454 | Get the name of the test case 455 | :returns: string 456 | """ 457 | 458 | def getTestName(self): 459 | return self.metadata.name 460 | 461 | """ 462 | Get the severity if the test case failed 463 | :returns: Definitions macro 464 | """ 465 | 466 | def getSeverity(self): 467 | return self.metadata.severity 468 | 469 | """ 470 | Get the ease of execution 471 | :returns: Definitions macro 472 | """ 473 | 474 | def getEaseOfExec(self): 475 | return self.metadata.ease 476 | 477 | """ 478 | Prints a message depends on the test case result 479 | """ 480 | 481 | def printMsg(self, passed): 482 | if (not passed and not self.getCritical()): 483 | self.info.log("- severity: " + self.getSeverity() + "; ease: " 484 | + self.getEaseOfExec() + "; see " + 485 | self.getReference()) 486 | -------------------------------------------------------------------------------- /src/TestCases.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file contains the terminal, or leaf, test case classes. They are the 3 | "bread and butter" of the software. Each class represents a test case under 4 | the group defined by its ancestors. Each test case begins by creating a 5 | TestMetadata object to describe itself. Then, the test case calls its parent 6 | constructor to build a basic test case along with some additional 7 | functions provided by the ancestors. Then, depending on the test case, further 8 | modification can be made. Cases that are suitable to be expanded into 9 | another case can do so by changing its attributes in TestMetadata 10 | object. 11 | 12 | NOTE: No certificate is written to hard disk by merely calling the constructor. 13 | The actual creation of certificates is performed once the build() method 14 | is called (by TestSet). Same is true for the actual keys used in the 15 | signing process. Those are design features that improves the performance 16 | of the software, as well as increases the flexibility on modifying 17 | components of the test case during construction. 18 | 19 | @author: Calvin Jia Liang 20 | Created on May 29, 2014 21 | """ 22 | 23 | # ############################################################################ 24 | # Parameter Descriptions 25 | # 26 | # fqdn - string; fully quantifiable domain name 27 | # metadata - Metadata object; check definition in the Metadata class 28 | # info - Information object; check definition in the Information class 29 | # depth - integer; number of certificates in the chain (including server 30 | # certificate but excluding root CA) 31 | # 32 | # 33 | # Basic Terminology 34 | # 35 | # certificate - an asn1 data structure specified by the RFC5280; 36 | # mainly refers to the X509 version 3 certificate 37 | # basic certificate - certificate without any extension 38 | # test certificate - certificate that potentially contains flaws 39 | # server certificate - certificate used to bind the identify and public key 40 | # of the target system (usually a SSL/TLS server) 41 | # CA - certificate authority that acts as a trusted third party in CA-PKI model; 42 | # it has the power to sign other certificates 43 | # root CA - certificate authority that has a self-signed certificate installed 44 | # in the system's trust store 45 | # intermediate CA - certificate authority that is signed by another CA 46 | # edge/leaf CA - certificate authority that signs the server certificate 47 | # CN - common name in the subject distinguish name field 48 | # 49 | # More background information available in RFC5280, RFC6125, RFC 5246, etc. 50 | # ############################################################################ 51 | 52 | 53 | from src.TestGroups import * 54 | 55 | 56 | # Basic Valid Cert Test #################################################### 57 | 58 | class ValidCert(TestCase): 59 | 60 | """A valid certificate, signed directly by the root CA, that contains its 61 | fqdn in the CN.""" 62 | 63 | def __init__(self, fqdn, info): 64 | metadata = TestMetadata(self.__class__.__name__, None, None, None, 65 | True, True) 66 | super(self.__class__, self).__init__(fqdn, metadata, info) 67 | 68 | # Immediate Cert Tests #################################################### 69 | 70 | class InvalidName(TestCaseImmed): 71 | 72 | """A basic certificate that contains an incorrect fqdn in the CN.""" 73 | 74 | def __init__(self, fqdn, info): 75 | metadata = TestMetadata(self.__class__.__name__, "RFC6125 6.4", 76 | SEV_MED, EASE_HIGH, altextend=True) 77 | super(self.__class__, self).__init__(getInvalidDomain(fqdn), metadata, 78 | info) 79 | 80 | class InvalidNameNull(TestCaseImmed): 81 | 82 | """A basic certificate that contains a null-prefix attack in the CN.""" 83 | 84 | def __init__(self, fqdn, info): 85 | metadata = TestMetadata(self.__class__.__name__, "RFC6125 6.4", 86 | SEV_MED, EASE_MED) 87 | super(self.__class__, self).__init__(fqdn, metadata, info) 88 | 89 | self.getServCert().modifier.hasPreSign = True 90 | self.getServCert().modifier.preSign = self.preSign 91 | 92 | def preSign(self, asnObj): 93 | (asnObj 94 | .getComponentByPosition(0) 95 | .getComponentByName('subject') 96 | .getComponentByPosition(0) 97 | .getComponentByPosition(5) 98 | .getComponentByPosition(0) 99 | .setComponentByName('value', 100 | rfc2459.TeletexCommonName( 101 | getInvalidNullDomain(self.fqdn).encode('utf-8')))) 102 | return asnObj 103 | 104 | 105 | 106 | class InvalidNotBefore(TestCaseImmed): 107 | 108 | """A basic certificate that contains a NotBefore time after now.""" 109 | 110 | def __init__(self, fqdn, info): 111 | metadata = TestMetadata(self.__class__.__name__, "RFC5280 4.1.2.5", 112 | SEV_MED, EASE_HIGH, chainable=True) 113 | super(self.__class__, self).__init__(fqdn, metadata, info) 114 | 115 | self.getServCert().security.notBefore = HOUR_DISCREPANCY 116 | 117 | 118 | class InvalidNotAfter(TestCaseImmed): 119 | 120 | """A basic certificate that contains a NotAfter time before now.""" 121 | 122 | def __init__(self, fqdn, info): 123 | metadata = TestMetadata(self.__class__.__name__, "RFC5280 4.1.2.5", 124 | SEV_MED, EASE_HIGH, chainable=True) 125 | super(self.__class__, self).__init__(fqdn, metadata, info) 126 | 127 | self.getServCert().security.notAfter = -HOUR_DISCREPANCY 128 | 129 | 130 | class InvalidIntegrity(TestCaseImmed): 131 | 132 | """A basic certificate that has been modified after its signature.""" 133 | 134 | def __init__(self, fqdn, info): 135 | metadata = TestMetadata(self.__class__.__name__, "PKCS7", SEV_MED, 136 | EASE_HIGH, chainable=True) 137 | super(self.__class__, self).__init__(getInvalidDomain(fqdn), metadata, 138 | info) 139 | 140 | self.fqdnValid = fqdn 141 | self.getServCert().modifier.hasPostSign = True 142 | self.getServCert().modifier.postSign = self.postSign 143 | 144 | def postSign(self, asnObj): 145 | (asnObj 146 | .getComponentByPosition(0) 147 | .getComponentByName('subject') 148 | .getComponentByPosition(0) 149 | .getComponentByPosition(5) 150 | .getComponentByPosition(0) 151 | .setComponentByName('value', rfc2459.TeletexCommonName(self.fqdnValid.encode('utf-8')))) 152 | return asnObj 153 | 154 | 155 | class InvalidExtendedKeyUsage(TestCaseImmed): 156 | 157 | """A certificate that has an unsuitable value in the extended key usage 158 | extension (serverAuth=false).""" 159 | 160 | def __init__(self, fqdn, info): 161 | metadata = TestMetadata(self.__class__.__name__, "RFC5280 4.2.1.12", 162 | SEV_LOW, EASE_LOW) 163 | super(self.__class__, self).__init__(fqdn, metadata, info) 164 | 165 | self.getServCert().addExtension( 166 | ExtendedKeyUsage( 167 | serverAuth=False, 168 | clientAuth=True, 169 | codeSigning=True, 170 | emailProtection=True, 171 | timeStamping=True)) 172 | 173 | 174 | class InvalidKeyUsage(TestCaseImmed): 175 | 176 | """A certificate that has an unsuitable value in the key usage extension 177 | (cRLSign=true).""" 178 | 179 | def __init__(self, fqdn, info): 180 | metadata = TestMetadata(self.__class__.__name__, "RFC5280 4.2.1.3", 181 | SEV_LOW, EASE_LOW) 182 | super(self.__class__, self).__init__(fqdn, metadata, info) 183 | 184 | self.getServCert().addExtension(KeyUsage(cRLSign=True)) 185 | 186 | 187 | class UnknownCriticalExtension(TestCaseImmed): 188 | 189 | """A certificate that has an non-standard critical extension entry.""" 190 | 191 | def __init__(self, fqdn, info): 192 | metadata = TestMetadata(self.__class__.__name__, "RFC5280 4.2", 193 | SEV_HIGH, EASE_MED, chainable=True) 194 | super(self.__class__, self).__init__(fqdn, metadata, info) 195 | 196 | self.getServCert().modifier.hasPreSign = True 197 | self.getServCert().modifier.preSign = self.preSign 198 | self.getServCert().addExtension(ExtendedKeyUsage(serverAuth=True, 199 | critical=True)) 200 | 201 | def preSign(self, asnObj): 202 | (asnObj 203 | .getComponentByPosition(0) 204 | .getComponentByName('extensions') 205 | .getComponentByPosition(0) 206 | .setComponentByName('extnID', 207 | rfc2459.univ.ObjectIdentifier(NONSTANDARD_OID))) 208 | return asnObj 209 | 210 | class UnknownNonCriticalExtension(TestCaseImmed): 211 | 212 | """A certificate that has an non-standard non-critical extension entry.""" 213 | 214 | def __init__(self, fqdn, info): 215 | metadata = TestMetadata(self.__class__.__name__, "RFC5280 4.2", 216 | SEV_LOW, EASE_MED, isValid=True, chainable=True) 217 | super(self.__class__, self).__init__(fqdn, metadata, info) 218 | 219 | self.getServCert().modifier.hasPreSign = True 220 | self.getServCert().modifier.preSign = self.preSign 221 | self.getServCert().addExtension(ExtendedKeyUsage(serverAuth=True, 222 | critical=False)) 223 | 224 | def preSign(self, asnObj): 225 | (asnObj 226 | .getComponentByPosition(0) 227 | .getComponentByName('extensions') 228 | .getComponentByPosition(0) 229 | .setComponentByName('extnID', 230 | rfc2459.univ.ObjectIdentifier(NONSTANDARD_OID))) 231 | return asnObj 232 | 233 | class InvalidSelfSign(TestCaseImmed): 234 | 235 | """A self-signed certificate.""" 236 | 237 | def __init__(self, fqdn, info): 238 | metadata = TestMetadata(self.__class__.__name__, "RFC5280 6.1.4", 239 | SEV_HIGH, EASE_HIGH) 240 | super(self.__class__, self).__init__(fqdn, metadata, info) 241 | 242 | self.getServCert().selfSign() 243 | 244 | 245 | # Chained Cert Tests #################################################### 246 | 247 | class ValidChained(TestCaseChained): 248 | 249 | """A valid chain of certificates. 250 | """ 251 | 252 | def __init__(self, fqdn, info): 253 | metadata = TestMetadata(self.__class__.__name__, "RFC5280", 254 | None, None, True, True) 255 | super(self.__class__, self).__init__(fqdn, metadata, info) 256 | 257 | 258 | class MissingIntCAExtensions(TestCaseChained): 259 | 260 | """A chain of certificates where the first intermediate CA is missing a 261 | basic constraint extension. 262 | """ 263 | 264 | def __init__(self, fqdn, info): 265 | metadata = TestMetadata(self.__class__.__name__, "RFC5280 4.2.1.9", 266 | SEV_HIGH, EASE_HIGH) 267 | super(self.__class__, self).__init__(fqdn, metadata, info) 268 | 269 | self.getFirstCA().removeExtension(BasicConstraint) 270 | self.getFirstCA().removeExtension(KeyUsage) 271 | 272 | 273 | 274 | 275 | class InvalidIntCAFlag(TestCaseChained): 276 | 277 | """A chain of certificates where the first intermediate CA has a basic 278 | constraint extension that is marked to false. 279 | """ 280 | 281 | def __init__(self, fqdn, info): 282 | metadata = TestMetadata(self.__class__.__name__, "RFC5280 4.2.1.9", 283 | SEV_HIGH, EASE_HIGH) 284 | super(self.__class__, self).__init__(fqdn, metadata, info) 285 | 286 | self.getFirstCA().getExtension(BasicConstraint).ca = False 287 | self.getFirstCA().removeExtension(KeyUsage) 288 | 289 | 290 | class ValidIntCALen(TestCaseChained): 291 | 292 | """A valid chain of certificates where the first intermediate CA and 293 | edge CA have basic constraint extensions that include pathLen of 5 and 0, 294 | respectively. 295 | """ 296 | 297 | def __init__(self, fqdn, info): 298 | metadata = TestMetadata(self.__class__.__name__, "RFC5280 4.2.1.9", 299 | SEV_LOW, EASE_LOW, False, True) 300 | super(self.__class__, self).__init__(fqdn, metadata, info) 301 | 302 | self.getFirstCA().getExtension(BasicConstraint).pathLen = 5 303 | self.getEdgeCA().getExtension(BasicConstraint).pathLen = 0 304 | 305 | 306 | 307 | class InvalidIntCALen(TestCaseChained): 308 | 309 | """A chain of certificates where the first and second intermediate CA 310 | have basic constraint extensions that both include pathLen of 1. 311 | """ 312 | 313 | def __init__(self, fqdn, info): 314 | metadata = TestMetadata(self.__class__.__name__, "RFC5280 4.2.1.9", 315 | SEV_HIGH, EASE_LOW) 316 | super(self.__class__, self).__init__(fqdn, metadata, info) 317 | 318 | self.getFirstCA().getExtension(BasicConstraint).pathLen = 1 319 | self.getSecondCA().getExtension(BasicConstraint).pathLen = 1 320 | 321 | 322 | 323 | class InvalidIntCAKeyUsage(TestCaseChained): 324 | 325 | """A chain of certificates where the first intermediate CA has keyCertSign 326 | marked as false in the key usage extension. 327 | """ 328 | 329 | def __init__(self, fqdn, info): 330 | metadata = TestMetadata(self.__class__.__name__, "RFC5280 4.2.1.3", 331 | SEV_HIGH, EASE_LOW) 332 | super(self.__class__, self).__init__(fqdn, metadata, info) 333 | 334 | self.getFirstCA().getExtension(KeyUsage).field['keyCertSign'] = False 335 | self.getFirstCA().getExtension(KeyUsage).field['digitalSignature'] = True 336 | self.getFirstCA().getExtension(KeyUsage).field['keyEncipherment'] = True 337 | 338 | class MissingIntCABasicConstraintWithCertSign(TestCaseChained): 339 | 340 | """A chain of certificates where the first intermediate CA has keyCertSign 341 | marked as true in the key usage extension but lacks the basic constraint 342 | extension entirely.""" 343 | 344 | def __init__(self, fqdn, info): 345 | metadata = TestMetadata(self.__class__.__name__, "RFC5280 4.2.1.3", 346 | SEV_HIGH, EASE_LOW) 347 | super(self.__class__, self).__init__(fqdn, metadata, info) 348 | 349 | self.getFirstCA().removeExtension(BasicConstraint) 350 | 351 | 352 | class InvalidIntCAVersionOne(TestCaseChained): 353 | 354 | """A chain of certificates where the first intermediate CA is a basic version 1 355 | certificate.""" 356 | 357 | def __init__(self, fqdn, info): 358 | metadata = TestMetadata(self.__class__.__name__, "RFC5280 6.1.4(k)", 359 | SEV_HIGH, EASE_HIGH) 360 | super(self.__class__, self).__init__(fqdn, metadata, info) 361 | 362 | self.getFirstCA().security.version = 0x00 363 | self.getFirstCA().extensions = [] 364 | 365 | # A chain of certificates where the first intermediate CA is a basic version 2 366 | # certificate 367 | 368 | 369 | class InvalidIntCAVersionTwo(TestCaseChained): 370 | 371 | def __init__(self, fqdn, info): 372 | metadata = TestMetadata(self.__class__.__name__, "RFC5280 6.1.4(k)", 373 | SEV_HIGH, EASE_HIGH) 374 | super(self.__class__, self).__init__(fqdn, metadata, info) 375 | 376 | self.getFirstCA().security.version = 0x01 377 | self.getFirstCA().extensions = [] 378 | 379 | # A chain of certificates where the first intermediate CA is signed by the 380 | # edge CA 381 | 382 | 383 | class InvalidIntCALoop(TestCaseChained): 384 | 385 | def __init__(self, fqdn, info): 386 | metadata = TestMetadata(self.__class__.__name__, "RFC5280 6.1.4", 387 | SEV_HIGH, EASE_HIGH) 388 | super(self.__class__, self).__init__(fqdn, metadata, info) 389 | 390 | self.getEdgeCA().security.certKey.build() 391 | self.getFirstCA().signer = CertSign( 392 | None, 393 | self.getEdgeCA().security.certKey, 394 | self.getEdgeCA().subject.getSubject()) 395 | 396 | # A chain of certificates where the first intermediate CA is self-signed 397 | 398 | 399 | class InvalidIntCASelfSign(TestCaseChained): 400 | 401 | def __init__(self, fqdn, info): 402 | metadata = TestMetadata(self.__class__.__name__, "RFC5280 6.1.4", 403 | SEV_HIGH, EASE_HIGH) 404 | super(self.__class__, self).__init__(fqdn, metadata, info) 405 | 406 | self.getFirstCA().selfSign() 407 | 408 | # Wildcard Cert Tests #################################################### 409 | 410 | # A valid wildcard certificate. 411 | 412 | 413 | class ValidWildcard(TestCaseWildcard): 414 | 415 | def __init__(self, fqdn, info): 416 | metadata = TestMetadata(self.__class__.__name__, "RFC6125 6.4.3", 417 | None, None, True, True) 418 | 419 | if (len(fqdn.split('.')) < 3): 420 | raise Exception("Input fqdn {} has too few components to generate" 421 | " wildcard tests".format(fqdn)) 422 | fqdnArr = fqdn.split('.') 423 | wildcard = "*." + '.'.join(fqdnArr[1:]) 424 | 425 | super(self.__class__, self).__init__(wildcard, metadata, info) 426 | 427 | # A wildcard certificate that tries to extend its matching effect to its left. 428 | # For example, it tries to match www.tls.test with *.test 429 | 430 | 431 | class InvalidWildcardLeft(TestCaseWildcard): 432 | 433 | def __init__(self, fqdn, info): 434 | metadata = TestMetadata(self.__class__.__name__, "RFC6125 6.4.3", 435 | SEV_MED, EASE_HIGH, altextend=True) 436 | 437 | fqdnArr = fqdn.split('.') 438 | wildcard = "*." + '.'.join(fqdnArr[2:]) 439 | 440 | super(self.__class__, self).__init__(wildcard, metadata, info) 441 | 442 | # A wildcard certificate that has wildcard character in the mid-segment of 443 | # the fqdn. 444 | # For example, it tries to match www.tls.test with www.*.test 445 | 446 | 447 | class InvalidWildcardMid(TestCaseWildcard): 448 | 449 | def __init__(self, fqdn, info): 450 | metadata = TestMetadata(self.__class__.__name__, "RFC6125 6.4.3", 451 | SEV_MED, EASE_MED, altextend=True) 452 | 453 | fqdnArr = fqdn.split('.') 454 | wildcard = fqdnArr[0] + ".*." + '.'.join(fqdnArr[2:]) 455 | 456 | super(self.__class__, self).__init__(wildcard, metadata, info) 457 | 458 | # A wildcard certificate that has wildcard character in the middle of the fqdn. 459 | # For example, it tries to match www.tls.test with www.*s.test 460 | 461 | 462 | class InvalidWildcardMidMixed(TestCaseWildcard): 463 | 464 | def __init__(self, fqdn, info): 465 | metadata = TestMetadata(self.__class__.__name__, "RFC6125 6.4.3", 466 | SEV_MED, EASE_MED, altextend=True) 467 | 468 | fqdnArr = fqdn.split('.') 469 | wildcard = fqdnArr[0] + ".*" + fqdnArr[1][-1] + "." +\ 470 | '.'.join(fqdnArr[2:]) 471 | 472 | super(self.__class__, self).__init__(wildcard, metadata, info) 473 | 474 | # A wildcard certificate that has wildcard characters in all segments. 475 | # For example, it tries to match www.tls.test with *.*.* 476 | 477 | 478 | class InvalidWildcardAll(TestCaseWildcard): 479 | 480 | def __init__(self, fqdn, info): 481 | metadata = TestMetadata(self.__class__.__name__, "RFC6125 6.4.3", 482 | SEV_MED, EASE_MED, altextend=True) 483 | 484 | fqdnArr = fqdn.split('.') 485 | for i in range(len(fqdnArr)): 486 | fqdnArr[i] = "*" 487 | wildcard = '.'.join(fqdnArr[:]) 488 | 489 | super(self.__class__, self).__init__(wildcard, metadata, info) 490 | 491 | # A wildcard certificate that has a single wildcard character. 492 | # For example, it tries to match www.tls.test with * 493 | 494 | 495 | class InvalidWildcardSingle(TestCaseWildcard): 496 | 497 | def __init__(self, fqdn, info): 498 | metadata = TestMetadata(self.__class__.__name__, "RFC6125 6.4.3", 499 | SEV_MED, EASE_MED, altextend=True) 500 | 501 | wildcard = "*" 502 | 503 | super(self.__class__, self).__init__(wildcard, metadata, info) 504 | 505 | 506 | # Alternate Name Cert Tests ################################################# 507 | 508 | # A valid certificate with a valid AltName but incorrect CN 509 | class ValidAltName(TestCaseAltName): 510 | 511 | def __init__(self, fqdn, info): 512 | metadata = TestMetadata(self.__class__.__name__, "RFC6125 6.4.4", 513 | None, None, True, True) 514 | super(self.__class__, self).__init__(fqdn, metadata, info) 515 | 516 | def printMsg(self, passed): 517 | TestCase.printMsg(self, passed) 518 | if (not passed): 519 | self.info.log("* NOTE: checking of SubjectAltName instead" + 520 | " of Common Name is encouraged; see RFC6125 1.5") 521 | 522 | # A certificate with a invalid AltName but correct CN 523 | 524 | 525 | class InvalidNameAltNameWithSubj(TestCaseAltName): 526 | 527 | def __init__(self, fqdn, info): 528 | metadata = TestMetadata(self.__class__.__name__, "RFC6125 6.4.4", 529 | SEV_LOW, EASE_LOW) 530 | super(self.__class__, self).__init__(fqdn, metadata, info) 531 | 532 | self.getServCert().subject.commonName = fqdn 533 | altNames = self.getServCert().getExtension(SubjectAltName).field['DNS'] 534 | for i in range(len(altNames)): 535 | altNames[i] = getInvalidDomain(fqdn) 536 | 537 | # A certificate with a null-prefix attack in its AltName 538 | 539 | 540 | class InvalidNameNullAltName(TestCaseAltName): 541 | 542 | def __init__(self, fqdn, info): 543 | metadata = TestMetadata(self.__class__.__name__, "RFC6125 6.4.4", 544 | SEV_MED, EASE_MED) 545 | super(self.__class__, self).__init__(fqdn, metadata, info) 546 | 547 | self.getServCert().modifier.hasPreSign = True 548 | self.getServCert().modifier.preSign = self.preSign 549 | self.trail = "x" + INVALID_TRAIL 550 | self.getServCert().getExtension(SubjectAltName).field['DNS'][0] = fqdn\ 551 | + self.trail 552 | 553 | def preSign(self, asnObj): 554 | val = asnObj.getComponentByPosition(0).getComponentByName('extensions')\ 555 | .getComponentByPosition(0).getComponentByName('extnValue') 556 | arr = bytearray(val._value) 557 | arr[-len(self.trail)] = 0 558 | val._value = bytes(arr) 559 | asnObj.getComponentByPosition(0).getComponentByName('extensions').\ 560 | getComponentByPosition(0).setComponentByName('extnValue', val) 561 | return asnObj 562 | 563 | # A certificate with a null-prefix attack in both its AltName and CN 564 | 565 | 566 | class InvalidNameNullAltNameAndSubj(TestCaseAltName): 567 | 568 | def __init__(self, fqdn, info): 569 | metadata = TestMetadata(self.__class__.__name__, "RFC6125 6.4.4", 570 | SEV_MED, EASE_MED) 571 | super(self.__class__, self).__init__(fqdn, metadata, info) 572 | 573 | self.getServCert().modifier.hasPreSign = True 574 | self.getServCert().modifier.preSign = self.preSign 575 | self.trail = "x" + INVALID_TRAIL 576 | self.getServCert().getExtension(SubjectAltName).field['DNS'][0] = \ 577 | fqdn + self.trail 578 | 579 | def preSign(self, asnObj): 580 | val = asnObj.getComponentByPosition(0).getComponentByName('extensions')\ 581 | .getComponentByPosition(0).getComponentByName('extnValue') 582 | arr = bytearray(val._value) 583 | arr[-len(self.trail)] = 0 584 | val._value = bytes(arr) 585 | asnObj.getComponentByPosition(0).getComponentByName('extensions').\ 586 | getComponentByPosition(0).setComponentByName('extnValue', val) 587 | 588 | asnObj.getComponentByPosition(0).getComponentByName('subject').\ 589 | getComponentByPosition(0).getComponentByPosition( 590 | 5). getComponentByPosition(0).setComponentByName('value', rfc2459.\ 591 | TeletexCommonName(getInvalidNullDomain(self.fqdn). encode('utf-8'))) 592 | 593 | return asnObj 594 | 595 | # A certificate with a null-prefix attack in its AltName but a correct CN 596 | 597 | 598 | class InvalidNameNullAltNameWithSubj(TestCaseAltName): 599 | 600 | def __init__(self, fqdn, info): 601 | metadata = TestMetadata(self.__class__.__name__, "RFC6125 6.4.4", 602 | SEV_LOW, EASE_MED) 603 | super(self.__class__, self).__init__(fqdn, metadata, info) 604 | 605 | self.getServCert().subject.commonName = fqdn 606 | self.getServCert().modifier.hasPreSign = True 607 | self.getServCert().modifier.preSign = self.preSign 608 | self.trail = "x" + INVALID_TRAIL 609 | self.getServCert().getExtension(SubjectAltName).field['DNS'][0] = \ 610 | fqdn + self.trail 611 | 612 | def preSign(self, asnObj): 613 | val = asnObj.getComponentByPosition(0).getComponentByName('extensions')\ 614 | .getComponentByPosition(0).getComponentByName('extnValue') 615 | arr = bytearray(val._value) 616 | arr[-len(self.trail)] = 0 617 | val._value = bytes(arr) 618 | asnObj.getComponentByPosition(0).getComponentByName('extensions').\ 619 | getComponentByPosition(0).setComponentByName('extnValue', val) 620 | return asnObj 621 | 622 | # Name Constraint Cert Tests ################################################# 623 | 624 | # A valid chain with a permitted subtree of ".test" in the first intermediate CA 625 | # and an excluded subtree of an incorrect fqdn in the second intermediate CA. 626 | 627 | 628 | class ValidNameConstraint(TestCaseNameConstraint): 629 | 630 | def __init__(self, fqdn, info): 631 | metadata = TestMetadata(self.__class__.__name__, "RFC5280 4.2.1.10", 632 | None, None, True, True) 633 | 634 | if (len(fqdn.split('.')) < 2): 635 | raise Exception("Input fqdn %s has too few tokens to generate" + 636 | " name constraints tests" % fqdn) 637 | super(self.__class__, self).__init__(fqdn, metadata, info) 638 | 639 | self.appendPermit(self.getFirstCA(), '.' + fqdn.split('.')[-1]) 640 | self.appendExclude(self.getSecondCA(), getInvalidDomain(fqdn)) 641 | 642 | # A chain with an excluded subtree of the fqdn's network name in the 643 | # first intermediate CA. 644 | 645 | 646 | class InvalidNameConstraintExclude(TestCaseNameConstraint): 647 | 648 | def __init__(self, fqdn, info): 649 | metadata = TestMetadata(self.__class__.__name__, "RFC5280 4.2.1.10", 650 | SEV_MED, EASE_LOW) 651 | super(self.__class__, self).__init__(fqdn, metadata, info) 652 | 653 | self.appendExclude(self.getFirstCA(), '.' + fqdn.split('.')[-1]) 654 | 655 | # A chain with an excluded subtree of any in the first intermediate CA. 656 | # class InvalidNameConstraintPermitNone(TestCaseNameConstraint): 657 | # def __init__(self, fqdn, info): 658 | # metadata = TestMetadata(self.__class__.__name__, "RFC5280 4.2.1.10",\ 659 | # SEV_MED, EASE_LOW); 660 | # super(self.__class__, self).__init__(fqdn, metadata, info); 661 | # 662 | # self.appendPermit(self.getFirstCA(), ""); 663 | 664 | # A chain with a permitted subtree of an incorrect fqdn's network name in the 665 | # first intermediate CA. 666 | 667 | 668 | class InvalidNameConstraintPermit(TestCaseNameConstraint): 669 | 670 | def __init__(self, fqdn, info): 671 | metadata = TestMetadata(self.__class__.__name__, "RFC5280 4.2.1.10", 672 | SEV_MED, EASE_LOW) 673 | super(self.__class__, self).__init__(fqdn, metadata, info) 674 | 675 | self.appendPermit(self.getFirstCA(), '.' + fqdn.split('.')[-1] + "x") 676 | 677 | # A chain with a permitted subtree of a truncated fqdn's network name in the 678 | # first intermediate CA. 679 | 680 | 681 | class InvalidNameConstraintPermitRight(TestCaseNameConstraint): 682 | 683 | def __init__(self, fqdn, info): 684 | metadata = TestMetadata(self.__class__.__name__, "RFC5280 4.2.1.10", 685 | SEV_MED, EASE_LOW) 686 | super(self.__class__, self).__init__(fqdn, metadata, info) 687 | 688 | self.appendPermit(self.getFirstCA(), '.' + fqdn.split('.')[-2]) 689 | 690 | # A chain with a permitted subtree of ".test" in the first intermediate CA and 691 | # an excluded subtree of ".test" in the second intermediate CA. 692 | 693 | 694 | class InvalidNameConstraintPermitThenExclude(TestCaseNameConstraint): 695 | 696 | def __init__(self, fqdn, info): 697 | metadata = TestMetadata(self.__class__.__name__, "RFC5280 4.2.1.10", 698 | SEV_MED, EASE_LOW) 699 | super(self.__class__, self).__init__(fqdn, metadata, info) 700 | 701 | self.appendPermit(self.getFirstCA(), '.' + fqdn.split('.')[-1]) 702 | self.appendExclude(self.getSecondCA(), '.' + fqdn.split('.')[-1]) 703 | -------------------------------------------------------------------------------- /src/TestExpander.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file holds the test expansion logic that expands certain marked 3 | test cases into a new and separate test case. It does so by swapping 4 | "body parts" from one test case to another test case. Probably the most 5 | fragile part of the software. (This part of the code caused more crashes 6 | than any other part of the software) 7 | 8 | @author: Calvin Jia Liang 9 | Created on Sep 6, 2014 10 | """ 11 | 12 | from src.TestCases import * 13 | 14 | # TestExpander class that checks possible candidates and creates a new test 15 | # case that is similar to the original one 16 | 17 | 18 | class TestExpander: 19 | 20 | """ 21 | TestExpander constructor 22 | :param fqdn: fully quantifiable domain name of the test case 23 | :type fqdn: string 24 | :param info: other information for the test session 25 | :type info: Information object 26 | :returns: TestExpander object 27 | """ 28 | 29 | def __init__(self, fqdn, info): 30 | self.fqdn = fqdn 31 | self.info = info 32 | 33 | self.expansions = {} 34 | 35 | """ 36 | Create a new chained test case by replacing the new case's first CA 37 | certificate with the candidate's server certificate 38 | :param case: test case to be examined 39 | :type case: TestCase object 40 | :returns: TestCase object; None if candidate is not suitable for 41 | this expansion 42 | """ 43 | 44 | def expandChainable(self, case): 45 | rtn = None 46 | 47 | if (case.isChainable() and not isinstance(case, TestCaseChained)): 48 | certName = getIntCAName(1) 49 | t = case.__class__(certName, self.info) 50 | t.metadata.name = getChainedName(t.metadata.name) 51 | 52 | t.metadata.ease = EASE_LOW 53 | t.metadata.severity = SEV_HIGH 54 | 55 | if (t.metadata.name == InvalidIntegrity.__name__): 56 | t.metadata.ease = EASE_HIGH 57 | 58 | newClass = type(t.metadata.name, ValidChained.__bases__, 59 | dict(ValidChained.__dict__)) 60 | rtn = newClass(self.fqdn, self.info) 61 | rtn = rtn.newTestCaseChained(self.fqdn, t.metadata, self.info, 62 | rtn.depth) 63 | rtn.replaceCA(0, t.getServCert()) 64 | 65 | self.expansions[t.metadata.name] = t.metadata.name 66 | self.insertBaseClass(rtn, t) 67 | 68 | return rtn 69 | 70 | """ 71 | Create a new altname test case by adding the candidate server certificate's 72 | CN to the AltName extension as a DNS-ID 73 | :param case: test case to be examined 74 | :type case: TestCase object 75 | :returns: TestCase object; None if candidate is not suitable for 76 | this expansion 77 | """ 78 | 79 | def expandAltExtend(self, case): 80 | rtn = None 81 | 82 | if (case.isAltExtend() and not isinstance(case, TestCaseAltName)): 83 | t = case.__class__(self.fqdn, self.info) 84 | t.metadata.name = getAltExtendedName(t.metadata.name) 85 | 86 | newClass = type(t.metadata.name, ValidAltName.__bases__, 87 | dict(ValidAltName.__dict__)) 88 | newClass.printMsg = TestCase.printMsg 89 | rtn = newClass(self.fqdn, self.info) 90 | rtn = rtn.newTestCaseAltName(self.fqdn, t.metadata, self.info) 91 | 92 | fieldName = 'IP' if (isIPAddr(rtn.fqdn) and rtn.info.useAddr)\ 93 | else 'DNS' 94 | rtn.getServCert().getExtension(SubjectAltName).\ 95 | field[fieldName][0] = t.getServCert().security.fqdn 96 | 97 | self.expansions[t.metadata.name] = t.metadata.name 98 | self.insertBaseClass(rtn, t) 99 | 100 | return rtn 101 | 102 | """ 103 | Get if the class in question is created as the result of the expansion 104 | process; this function is used to make sure that the same expanded test 105 | does not get build twice (which happens because type() inserts a new 106 | class entry during runtime) 107 | :param classObj: test case's class to be examined 108 | :type classObj: Class object 109 | :returns: boolean 110 | """ 111 | 112 | def isExpansion(self, classObj): 113 | return hasattr(classObj, '__name__') and classObj.__name__ in\ 114 | self.expansions 115 | 116 | """ 117 | Get a list of extensions for the test case; new expansions must be included 118 | to this function for it to become active 119 | :param case: test case to be examined 120 | :type case: TestCase object 121 | :returns: list of TestCase object 122 | """ 123 | 124 | def getExpansions(self, case): 125 | rtn = [] 126 | 127 | chain = self.expandChainable(case) 128 | alt = self.expandAltExtend(case) 129 | 130 | if (chain): 131 | rtn.append(chain) 132 | if (alt): 133 | rtn.append(alt) 134 | return rtn 135 | 136 | """ 137 | Insert the parent class of one object to another 138 | :param baseObj: destination of the insertion 139 | :type baseObj: TestCase object 140 | :param inObj: source of the insertion 141 | :type inObj: TestCase object 142 | """ 143 | 144 | def insertBaseClass(self, baseObj, inObj): 145 | if (not isinstance(baseObj, inObj.__class__.__bases__[0]) and 146 | not isinstance(inObj, baseObj.__class__.__bases__[0])): 147 | baseObj.__class__.__bases__ += (inObj.__class__.__bases__[0],) 148 | -------------------------------------------------------------------------------- /src/TestFunctionality.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file contains the automatic test generation logic that generates VALID 3 | and POSITIVE test cases under different configuration. It is used to test 4 | the different handshake settings in which the other end is willing to accept. 5 | 6 | NOTE: Even the test cases generated is valid on paper, it is still critical 7 | for the other end to reject some of its test cases. Settings like 8 | anonymous Diffie-Hellman should never be accepted by any secure 9 | entity. 10 | 11 | @author: Calvin Jia Liang 12 | Created on Sep 7, 2014 13 | """ 14 | 15 | from src.TestGroups import * 16 | 17 | 18 | class TestFunctionality: 19 | 20 | """ 21 | TestFunctionality constructor 22 | :param fqdn: fully quantifiable domain name 23 | :type fqdn: string 24 | :param info: other information for the test session 25 | :type info: Information object 26 | :param sizes: key lengths to test 27 | :type sizes: list of integer 28 | :param types: type of certificates to test 29 | :type types: list of pyOpenSSL macro 30 | :param suites: cipher suites to test 31 | :type suites: list of string 32 | :param versions: type of SSL/TLS versions to test 33 | :type versions: list of pyOpenSSL macro 34 | :returns: TestFunctionality object 35 | """ 36 | 37 | def __init__(self, fqdn, info): 38 | self.fqdn = fqdn 39 | self.info = copy.copy(info) 40 | self.info.metadata = None 41 | 42 | self.sizes = FUNC_KEY_SIZES 43 | self.types = FUNC_KEY_TYPES 44 | self.hashes = FUNC_HASH_TYPES 45 | self.suites = FUNC_CIPHER_SUITES 46 | self.versions = FUNC_SSL_VERSIONS 47 | self.cases = [] 48 | 49 | """ 50 | Build a list of functionality test cases based on the input given; they are 51 | positive test cases that only differ in one or two components 52 | :returns: TestFunctionality object 53 | """ 54 | 55 | def build(self): 56 | metadata = TestMetadata("", "", None, None, False, True, 57 | functional=True) 58 | 59 | for ktype in self.types: 60 | for size in self.sizes: 61 | mdata = copy.copy(metadata) 62 | mdata.name = self.getKeyName(ktype, size) 63 | mdata.suite = "ALL" 64 | 65 | case = TestCase(self.fqdn, mdata, self.info) 66 | case.getServCert().security.certKey = CertKey(None, kSize=size, 67 | kType=ktype) 68 | 69 | self.cases.append(case) 70 | 71 | for version in self.versions: 72 | for suite in self.suites: 73 | mdata = copy.copy(metadata) 74 | mdata.name = self.getSuiteName(version, suite) 75 | mdata.sslVer = version 76 | mdata.suite = suite 77 | 78 | case = TestCase(self.fqdn, mdata, self.info) 79 | self.cases.append(case) 80 | 81 | for version in self.versions: 82 | for digest in self.hashes: 83 | mdata = copy.copy(metadata) 84 | mdata.name = self.getHashName(version, digest) 85 | mdata.sslVer = version 86 | mdata.suite = "ALL" 87 | 88 | case = TestCase(self.fqdn, mdata, self.info) 89 | case.getServCert().security.digest = digest 90 | 91 | self.cases.append(case) 92 | 93 | return self 94 | 95 | def getKeyName(self, ktype, size): 96 | name = "" 97 | 98 | if (ktype == crypto.TYPE_RSA): 99 | name += "RSA" 100 | elif (ktype == crypto.TYPE_DSA): 101 | name += "DSA" 102 | else: 103 | forcedExit("Unknown Key Type.", self.info.log) 104 | name += "_" + str(size) 105 | 106 | return name 107 | 108 | def getHashName(self, version, digest): 109 | return self.getVersionName(version) + '_' + digest 110 | 111 | def getSuiteName(self, version, suite): 112 | return self.getVersionName(version) + '_' + suite 113 | 114 | def getVersionName(self, sver): 115 | ver = None 116 | 117 | if (sver == SSL.SSLv23_METHOD): 118 | ver = "SSLv23" 119 | elif (sver == SSL.SSLv2_METHOD): 120 | ver = "SSLv2" 121 | elif (sver == SSL.SSLv3_METHOD): 122 | ver = "SSLv3" 123 | elif (sver == SSL.TLSv1_METHOD): 124 | ver = "TLSv1_0" 125 | elif (sver == SSL.TLSv1_1_METHOD): 126 | ver = "TLSv1_1" 127 | elif (sver == SSL.TLSv1_2_METHOD): 128 | ver = "TLSv1_2" 129 | else: 130 | forcedExit("Unknown SSL/TLS Version.", self.info.log) 131 | 132 | return ver 133 | 134 | def getAllNames(self, tbl, exclude): 135 | for ktype in self.types: 136 | for size in self.sizes: 137 | name = self.getKeyName(ktype, size) 138 | if (name not in exclude): 139 | tbl[name] = name 140 | 141 | for version in self.versions: 142 | for suite in self.suites: 143 | name = self.getSuiteName(version, suite) 144 | if (name not in exclude): 145 | tbl[name] = name 146 | 147 | for version in self.versions: 148 | for digest in self.hashes: 149 | name = self.getHashName(version, digest) 150 | if (name not in exclude): 151 | tbl[name] = name 152 | 153 | return tbl 154 | 155 | """ 156 | Get the list of test cases created from this object 157 | :returns: list of TestCase object 158 | """ 159 | 160 | def getTestCases(self): 161 | return self.cases 162 | -------------------------------------------------------------------------------- /src/TestGroups.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file holds all (abstract) classes that are descendant of TestCase. 3 | They are the intermediate nodes in the test case inheritance hierarchy. 4 | They help to organize test cases into different groups for better management. 5 | Some provide additional functionalities for their descendants. 6 | 7 | @author: Calvin Jia Liang 8 | Created on Sep 3, 2014 9 | """ 10 | 11 | # ############################################################################ 12 | # Parameter Descriptions 13 | # 14 | # fqdn - string; fully quantifiable domain name 15 | # metadata - Metadata object; check definition in the Metadata class 16 | # info - Information object; check definition in the Information class 17 | # depth - integer; number of certificates in the chain (including server 18 | # certificate but excluding root CA) 19 | # 20 | # 21 | # Basic Terminology 22 | # 23 | # certificate - an asn1 data structure specified by the RFC5280; 24 | # mainly refers to the X509 version 3 certificate 25 | # basic certificate - certificate without any extension 26 | # test certificate - certificate that potentially contains flaws 27 | # server certificate - certificate used to bind the identify and public key 28 | # of the target system (usually a SSL/TLS server) 29 | # CA - certificate authority that acts as a trusted third party in CA-PKI model; 30 | # it has the power to sign other certificates 31 | # root CA - certificate authority that has a self-signed certificate installed 32 | # in the system's trust store 33 | # intermediate CA - certificate authority that is signed by another CA 34 | # edge/leaf CA - certificate authority that signs the server certificate 35 | # CN - common name in the subject distinguish name field 36 | # 37 | # More background information available in RFC5280, RFC6125, RFC 5246, etc. 38 | # ############################################################################ 39 | 40 | 41 | from src.Test import * 42 | 43 | 44 | # Immediate test case group where a basic server certificate is directly 45 | # signed by the root certificate. 46 | # The server certificate is the test certificate. 47 | class TestCaseImmed(TestCase): 48 | pass 49 | 50 | # Chained test case group where a basic server certificate is signed by an 51 | # intermediate CA, which ultimately linked to the root certificate (if signed 52 | # correctly). 53 | # The intermediate CAs (usually the one directly signed by the root CA) 54 | # is the test certificate. 55 | 56 | 57 | class TestCaseChained(TestCase): 58 | 59 | def __init__(self, fqdn, metadata, info, depth=DEFAULT_NUM_CHAINED): 60 | self.newTestCaseChained(fqdn, metadata, info, depth) 61 | 62 | def newTestCaseChained(self, fqdn, metadata, info, depth): 63 | super(TestCaseChained, self).__init__(fqdn, metadata, info, depth) 64 | return self 65 | 66 | # Wildcard test case group where a server certificate uses wildcard characters 67 | # to identify itself. 68 | # The server certificate is the test certificate. 69 | 70 | 71 | class TestCaseWildcard(TestCase): 72 | pass 73 | 74 | # AltName test case group where some certificates contains the Alternative Name 75 | # Extension. 76 | # It will render the CN of the server certificate incorrectly by default. 77 | 78 | 79 | class TestCaseAltName(TestCase): 80 | 81 | def __init__(self, fqdn, metadata, info): 82 | self.newTestCaseAltName(fqdn, metadata, info) 83 | 84 | def newTestCaseAltName(self, fqdn, metadata, info): 85 | super(TestCaseAltName, self).__init__(fqdn, metadata, info) 86 | 87 | self.includeAltName() 88 | self.getServCert().subject.commonName = getInvalidDomain(fqdn) 89 | 90 | return self 91 | 92 | # Chained test case group where some intermediate CA certificates contains 93 | # the Name Constraint Extension. 94 | # All certificates contains AltName extension by default. 95 | 96 | 97 | class TestCaseNameConstraint(TestCaseChained): 98 | 99 | def __init__(self, fqdn, metadata, info): 100 | self.newTestCaseNameConstraint(fqdn, metadata, info) 101 | 102 | def newTestCaseNameConstraint(self, fqdn, metadata, info): 103 | super(TestCaseNameConstraint, self).__init__(fqdn, metadata, info) 104 | 105 | self.includeAltName(critical=False) 106 | self.appendPermit(self.getFirstCA(), getIntCADomain()) 107 | 108 | return self 109 | 110 | def appendConstraint(self, cert, ln, k): 111 | ext = cert.getExtension(NameConstraints) 112 | ext = ext if ext else cert.addExtension(NameConstraints()) 113 | ext.field[k].append(ln) 114 | 115 | def appendPermit(self, cert, ln): 116 | self.appendConstraint(cert, ln, 'permitted') 117 | 118 | def appendExclude(self, cert, ln): 119 | self.appendConstraint(cert, ln, 'excluded') 120 | -------------------------------------------------------------------------------- /src/TestOverflow.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file contains the automatic test generation logic that generates test cases 3 | with exactly one basic attribute containing a long string. It is used to test 4 | the overflow resistant capability of the other end. The main criteria for 5 | passing this set of test cases is that the other end does not crash. The minor 6 | criteria is that the other end aborts the connection. 7 | 8 | @author: Calvin Jia Liang 9 | Created on Oct 11, 2014 10 | """ 11 | 12 | from src.TestGroups import * 13 | 14 | 15 | class TestOverflow: 16 | 17 | NAME_TABLE = ["SignatureAlgorithm", "SubjectAltName", 18 | "BasicConstraint", "KeyUsage", 19 | "ExtendedKeyUsage", "IssuerC", "IssuerST", 20 | "IssuerL", "IssuerO", "IssuerOU", "IssuerCN", 21 | "IssuerEmail", "SubjectC", "SubjectST", 22 | "SubjectL", "SubjectO", "SubjectOU", "SubjectCN", 23 | "SubjectEmail"] 24 | 25 | NAME_TABLE_EXT = ["LongChain", "LongExtension", "LongOID"] 26 | 27 | """ 28 | TestOverflow constructor 29 | :param fqdn: fully quantifiable domain name 30 | :type fqdn: string 31 | :param info: other information for the test session 32 | :type info: Information object 33 | :param length: byte length of the overflow filler 34 | :type length: integer 35 | :param validCA: asssert if the CA of this test set is valid 36 | :type validCA: boolean 37 | :returns: TestOverflow object 38 | """ 39 | 40 | def __init__(self, fqdn, info, overflowLen=DEFAULT_OVERFLOW_LENGTH): 41 | self.fqdn = fqdn 42 | self.info = copy.copy(info) 43 | self.info.metadata = None 44 | self.overflowLen = overflowLen 45 | self.validCA = OVERFLOW_VALID_CA 46 | 47 | self.step = 0 48 | self.filler = None 49 | self.cases = [] 50 | 51 | """ 52 | Build a list of overflow test cases based on basic attributes of the 53 | certificate; they are mostly negative test cases that has exactly one 54 | attribute containing a long string 55 | :returns: TestOverflow object 56 | """ 57 | 58 | def build(self): 59 | baseCase = self.newSubstrate("TestOverflowBaseCase") 60 | baseCase.getServCert().modifier.hasPreSign = False 61 | baseCase.testBuild(replace=True) 62 | cert = baseCase.getServCert().getCert() 63 | substrate = crypto.dump_certificate(crypto.FILETYPE_ASN1, cert) 64 | cert = decoder.decode(substrate, asn1Spec=rfc2459.Certificate())[0] 65 | 66 | cnt = self.countBasicAttr(cert) 67 | if (cnt != len(TestOverflow.NAME_TABLE)): 68 | raise Exception("Attribute count and name table length mismatch") 69 | 70 | tempCases = [] 71 | for i in range(cnt): 72 | tempCases.append(self.newSubstrate(self.getName(i))) 73 | tempCases.append(self.getLongChain()) 74 | tempCases.append(self.getLongExtension()) 75 | # tempCases.append(self.getLongAttribute()) 76 | tempCases.append(self.getLongOID()) 77 | 78 | for c in tempCases: 79 | if (not self.validCA): 80 | c.getFirstCA().selfSign() 81 | self.cases.append(c) 82 | 83 | return self 84 | 85 | 86 | def getName(self, idx): 87 | return "Long" + TestOverflow.NAME_TABLE[idx] 88 | 89 | """ 90 | Get a new test case substrate 91 | :param name: name of the test case 92 | :type name: string 93 | :returns: Certificate object 94 | """ 95 | 96 | def newSubstrate(self, name): 97 | metadata = TestMetadata(name, "", None, None, False, False, 98 | overflow=True) 99 | 100 | substrate = TestCaseChained(self.fqdn, metadata, self.info, 2) 101 | substrate.includeAltName() 102 | substrate.getServCert().subject.commonName = self.fqdn 103 | substrate.getServCert().addExtension(BasicConstraint(False)) 104 | substrate.getServCert().addExtension(KeyUsage(keyEncipherment=True)) 105 | substrate.getServCert().addExtension(ExtendedKeyUsage(serverAuth=True)) 106 | 107 | substrate.getServCert().modifier.hasPreSign = True 108 | substrate.getServCert().modifier.preSign = self.preSignSubstrate 109 | 110 | return substrate 111 | 112 | """ 113 | Callback function to be executed before signature 114 | :param cert: certificate to be altered in asn1 format 115 | :type cert: pyasn1 object 116 | :returns: pyasn1 object 117 | """ 118 | 119 | def preSignSubstrate(self, cert): 120 | parent, idx = self.getState(cert, queue.Queue(), 0) 121 | 122 | comp = parent.getComponentByPosition(idx) 123 | if (comp._value == b'\x05\x00'): 124 | comp._value = b'\x07\x01' 125 | string = self.getFiller(self.overflowLen) 126 | comp._value = comp._value[0:1] + string 127 | 128 | self.step += 1 129 | return cert 130 | 131 | """ 132 | Get a long chained test case 133 | :returns: Certificate object 134 | """ 135 | 136 | def getLongChain(self): 137 | name = TestOverflow.NAME_TABLE_EXT[0] 138 | metadata = TestMetadata(name, "", None, None, False, False, 139 | overflow=True) 140 | 141 | substrate = TestCaseChained(self.fqdn, metadata, self.info, 142 | OVERFLOW_CHAIN_LEN) 143 | substrate.includeAltName() 144 | 145 | return substrate 146 | 147 | """ 148 | Get a long extension test case 149 | :returns: Certificate object 150 | """ 151 | 152 | def getLongExtension(self): 153 | name = TestOverflow.NAME_TABLE_EXT[1] 154 | metadata = TestMetadata(name, "", None, None, False, False, 155 | overflow=True) 156 | 157 | substrate = TestCaseChained(self.fqdn, metadata, self.info, 2) 158 | substrate.includeAltName(critical=False) 159 | base = substrate.getServCert().extensions[0] 160 | for _ in range(OVERFLOW_EXT_LEN): 161 | substrate.getServCert().extensions.append(base) 162 | 163 | return substrate 164 | 165 | # """ 166 | # Get a long attribute test case 167 | # :returns: Certificate object 168 | # """ 169 | # 170 | # def getLongAttribute(self): 171 | # name = "Overflow_LongAttribute" 172 | # metadata = TestMetadata(name, "", None, None, False, False, 173 | # overflow=True) 174 | # 175 | # substrate = TestCaseChained(self.fqdn, metadata, self.info, 2) 176 | # substrate.includeAltName() 177 | # substrate.getServCert().subject.commonName = self.fqdn 178 | # 179 | # substrate.getServCert().modifier.hasPreSign = True 180 | # substrate.getServCert().modifier.preSign = self.preSignAttribute 181 | # 182 | # return substrate 183 | # 184 | # """ 185 | # Callback function to be executed before signature 186 | # :param cert: certificate to be altered in asn1 format 187 | # :type cert: pyasn1 object 188 | # :returns: pyasn1 object 189 | # """ 190 | # 191 | # def preSignAttribute(self, cert): 192 | # comp = cert.getComponentByPosition(0).getComponentByName('extensions').\ 193 | # getComponentByPosition(0).getComponentByName('extnValue') 194 | # string = self.getFiller(self.overflowLen*OVERFLOW_MEGA_MUL) 195 | # comp._value = comp._value[0:1] + string 196 | # 197 | # return cert 198 | 199 | """ 200 | Get a long attribute test case 201 | :returns: Certificate object 202 | """ 203 | 204 | def getLongOID(self): 205 | name = TestOverflow.NAME_TABLE_EXT[2] 206 | metadata = TestMetadata(name, "", None, None, False, False, 207 | overflow=True) 208 | 209 | substrate = TestCaseChained(self.fqdn, metadata, self.info, 2) 210 | substrate.includeAltName(critical=False) 211 | 212 | substrate.getServCert().modifier.hasPreSign = True 213 | substrate.getServCert().modifier.preSign = self.preSignOID 214 | 215 | return substrate 216 | 217 | """ 218 | Callback function to be executed before signature 219 | :param cert: certificate to be altered in asn1 format 220 | :type cert: pyasn1 object 221 | :returns: pyasn1 object 222 | """ 223 | 224 | def preSignOID(self, cert): 225 | oid = ((NONSTANDARD_OID + '.') * OVERFLOW_OID_MUL)[:-1] 226 | (cert 227 | .getComponentByPosition(0) 228 | .getComponentByName('extensions') 229 | .getComponentByPosition(0) 230 | .setComponentByName('extnID', 231 | rfc2459.univ.ObjectIdentifier(oid))) 232 | return cert 233 | 234 | 235 | def getFiller(self, size): 236 | bLen = math.ceil((math.log(size+1)/math.log(2))/8) 237 | filler = bytes([128+bLen]) + \ 238 | size.to_bytes(bLen, 'big') + b'a'*size 239 | 240 | return filler 241 | 242 | 243 | """ 244 | Get the number basic attributes in the certificate 245 | :param cert: certificate to be counted 246 | :type cert: pyasn1 object 247 | :returns: integer 248 | """ 249 | 250 | def countBasicAttr(self, cert): 251 | cnt = 0 252 | self.step = 0 253 | 254 | while (True): 255 | parent, idx = self.getState(cert, queue.Queue(), 0) 256 | if (parent is None and idx is None): 257 | break 258 | self.step += 1 259 | cnt += 1 260 | 261 | self.step = 0 262 | return cnt-2 # exclude attr that only exist after signature 263 | 264 | """ 265 | Get the component and index of the certificate to be altered 266 | :param cert: certificate to be altered 267 | :type cert: pyasn1 object 268 | :param q: current queue 269 | :type q: Queue object 270 | :param s: current step 271 | :type s: integer 272 | :returns: asn1 object, integer 273 | """ 274 | 275 | def getState(self, cert, q, s): 276 | if (q.empty()): 277 | q.put((cert, None, None)) 278 | basic = False 279 | comp, parent, idx = q.get() 280 | 281 | if (comp.prettyPrint()[0:2] == '0x'): 282 | basic = True 283 | 284 | if (hasattr(comp, 'getComponentByPosition')): 285 | for i in range(len(comp)): 286 | sub = comp.getComponentByPosition(i) 287 | if (sub): 288 | q.put((sub, comp, i)) 289 | 290 | if (not basic or s != self.step): 291 | if (q.empty()): 292 | parent = idx = None 293 | elif (not basic): 294 | parent, idx = self.getState(cert, q, s) 295 | elif (s != self.step): 296 | parent, idx = self.getState(cert, q, s+1) 297 | 298 | return parent, idx 299 | 300 | def getAllNames(self, tbl, exclude): 301 | for i in range(len(TestOverflow.NAME_TABLE)): 302 | name = self.getName(i) 303 | if (name not in exclude): 304 | tbl[name] = name 305 | 306 | for name in TestOverflow.NAME_TABLE_EXT: 307 | if (name not in exclude): 308 | tbl[name] = name 309 | 310 | return tbl 311 | 312 | """ 313 | Get the list of test cases created from this object 314 | :returns: list of TestCase object 315 | """ 316 | 317 | def getTestCases(self): 318 | return self.cases 319 | -------------------------------------------------------------------------------- /src/TestServer.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file contains the SSL/TLS server logic that executes the given list 3 | of test cases. It establishes a SSL/TLS server, sends the parameters 4 | and certificates specified by the test case, and generate the result. 5 | If the server is able to read any data from the secured socket, it 6 | assumes the client accepts the test parameters. If an exception is 7 | encountered while trying to read from the same socket, it assumes the 8 | client reject the parameters. The server uses a new session for each 9 | test case. 10 | 11 | @author: Calvin Jia Liang 12 | Created on May 15, 2014 13 | """ 14 | 15 | from src.TestSet import * 16 | 17 | # TestServer class that represents a SSL/TLS test server. 18 | 19 | 20 | class TestServer: 21 | 22 | """ 23 | TestServer constructor 24 | :param testCases: list of test cases to be executed 25 | :type testCases: list of TestCase object 26 | :param baseCase: the basic and valid setting and parameters 27 | :type baseCase: TestCase object 28 | :param opt: user options 29 | :type opt: Terminal object (for now) 30 | :returns: TestServer object 31 | """ 32 | 33 | def __init__(self, testCases, baseCase, opt): 34 | self.testCases = testCases 35 | self.baseCase = baseCase 36 | self.opt = opt 37 | 38 | """ 39 | Initialize the server 40 | :param addr: IPv4 address of the server 41 | :type addr: string 42 | :param port: TCP port number to listen 43 | :type port: integer 44 | :param sslVer: SSL/TLS version 45 | :type sslVer: pyOpenSSL macro 46 | :returns: pyOpenSSL Connection object 47 | """ 48 | 49 | def initServer(self, addr, port, sslVer): 50 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 51 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 52 | server = SSL.Connection(SSL.Context(sslVer), sock) 53 | server.bind((addr, port)) 54 | server.listen(1) 55 | server.setblocking(True) 56 | return server 57 | 58 | """ 59 | Load the test server with a single test case and clean up afterward 60 | :param server: server that runs the test 61 | :type server: pyOpenSSL Connection object 62 | :param test: test case to run 63 | :type test: TestCase object 64 | :param sslVer: SSL/TLS version 65 | :type sslVer: pyOpenSSL macro 66 | :returns: boolean 67 | """ 68 | 69 | def runTest(self, server, test, sslVer): 70 | time.sleep(self.opt.pause) 71 | 72 | try: 73 | sslVer = test.getSSLVersion() if test.getSSLVersion() else sslVer 74 | ctx = SSL.Context(sslVer) 75 | ctx.use_privatekey_file(test.getKeyPath()) 76 | ctx.use_certificate_chain_file(test.getPemPath()) 77 | ctx.set_cipher_list(test.getCipherSuite()) 78 | 79 | server.set_context(ctx) 80 | except (ValueError, SSL.Error): 81 | return None 82 | 83 | rlist = [server] 84 | cont = True 85 | while (cont): 86 | try: 87 | if (VERBOSE): 88 | self.opt.log("Awaiting connection...") 89 | r, _, _ = select.select(rlist, [], []) 90 | except Exception as e: 91 | self.opt.log(str(e)) 92 | break 93 | 94 | for conn in r: 95 | if (conn == server): 96 | cli, _ = server.accept() 97 | rlist = [cli] 98 | elif (conn is not None): 99 | try: 100 | conn.recv(1024) 101 | connected = True 102 | except (SSL.WantReadError, SSL.WantWriteError, 103 | SSL.WantX509LookupError): 104 | if (VERBOSE): 105 | self.opt.log(str(e)) 106 | continue 107 | except (SSL.ZeroReturnError, SSL.Error) as e: 108 | if (VERBOSE): 109 | self.opt.log(str(e)) 110 | connected = False 111 | cont = False 112 | else: 113 | cont = False 114 | 115 | try: 116 | cli.shutdown() 117 | except SSL.Error as e: 118 | if (VERBOSE): 119 | self.opt.log(str(e)) 120 | 121 | return connected == test.getTestType() 122 | 123 | """ 124 | Execute a test case and determine the result based on different mode 125 | :param server: server that runs the test 126 | :type server: pyOpenSSL Connection object 127 | :param test: test case to run 128 | :type test: TestCase object 129 | :param sslVer: SSL/TLS version 130 | :type sslVer: pyOpenSSL macro 131 | :returns: boolean 132 | """ 133 | 134 | def execute(self, server, test, sslVer): 135 | if (not self.opt.diligent): 136 | passed = self.runTest(server, test, sslVer) 137 | else: 138 | out = 0 139 | for _ in range(REPEAT): 140 | cnt = 0 141 | for _ in range(REPEAT): 142 | passed = self.runTest(server, test, sslVer) 143 | cnt += 1 if passed else 0 144 | if ((cnt == 0 or cnt == REPEAT) and 145 | self.runTest(server, self.baseCase, sslVer)): 146 | break 147 | out += 1 148 | if (out == REPEAT): 149 | forcedExit("Invalid behavior encountered.") 150 | 151 | return passed 152 | 153 | """ 154 | Post processing and output of results; returns True if any case is removed 155 | :param passed: assert if test case passed; None for unsupported test case 156 | :type passed: boolean/None 157 | :param test: test case that just ran 158 | :type test: TestCase object 159 | :returns: boolean 160 | """ 161 | 162 | def output(self, passed, test): 163 | if (test.isFunctional()): 164 | if (passed is None): 165 | res = "? Unsupported" 166 | else: 167 | res = ": Accepted" if passed else "- REJECTED" 168 | self.opt.log("{:>16} {:}".format(test.getTestName(), res)) 169 | elif (test.isOverflow()): 170 | res = "| Terminated" if passed else ": Connected" 171 | self.opt.log("{:>24} {:}".format(test.getTestName(), res)) 172 | else: 173 | if ((passed and not self.opt.quiet) or not passed): 174 | if (passed is None): 175 | res = "Unsupported" 176 | else: 177 | res = "Passed" if passed else "FAILED" 178 | kind = "positive" if test.getTestType() else "negative" 179 | 180 | self.opt.log(res + " " + kind + " test: " + test.getTestName()) 181 | test.printMsg(passed) 182 | 183 | if (not passed and test.getCritical() and not self.opt.all): 184 | self.opt.log("- failed a dependency test, excluding " + 185 | "descendant test cases...") 186 | for c in self.testCases[:]: 187 | for p in test.__class__.__bases__: 188 | if (isinstance(c, p)): 189 | self.opt.log(" > " + str(c.getTestName())) 190 | self.testCases.remove(c) 191 | return True 192 | 193 | return False 194 | 195 | """ 196 | Run the test server 197 | :returns: boolean 198 | """ 199 | 200 | def run(self): 201 | nfails = 0 202 | ntests = 0 203 | 204 | addr = self.opt.addr 205 | port = self.opt.port 206 | sslVer = self.opt.sslVer 207 | 208 | self.opt.log("Starting Network Server...") 209 | server = self.initServer(addr, port, sslVer) 210 | 211 | self.opt.log("Server Ready!") 212 | self.opt.log("") 213 | 214 | i = 0 215 | while (i < len(self.testCases)): 216 | test = self.testCases[i] 217 | passed = self.execute(server, test, sslVer) 218 | rm = self.output(passed, test) 219 | 220 | if (not test.isFunctional() and not test.isOverflow()): 221 | nfails += 0 if passed else 1 222 | ntests += 1 223 | 224 | if (not rm): 225 | i += 1 226 | 227 | server.close() 228 | 229 | if (ntests): 230 | if (nfails): 231 | self.opt.log("Your SSL/TLS client did not pass " + 232 | str(nfails) + " test(s) out of " + 233 | str(ntests) + " total test(s).") 234 | else: 235 | self.opt.log("Congratulations! Your SSL/TLS client passed all " 236 | + str(ntests) + " test(s).") 237 | return True 238 | -------------------------------------------------------------------------------- /src/TestSet.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file contains the environment checking, option processing, and test 3 | building logic. The TestSet class builds the list of test cases by 4 | scanning all descendants of TestCase and calling the terminal 5 | descendant's build() method. Some of the user options are passed directly 6 | to the individual test case whereas others are processed locally. 7 | 8 | 9 | @author: Calvin Jia Liang 10 | Created on Sep 3, 2014 11 | """ 12 | 13 | from src.TestCases import * 14 | from src.TestExpander import * 15 | from src.TestFunctionality import * 16 | from src.TestOverflow import * 17 | 18 | 19 | # TestSet class represents an ordered list of test case OBJECT 20 | # readily available for execution. 21 | 22 | 23 | class TestSet: 24 | 25 | """ 26 | TestSet constructor 27 | :param fqdn: fully quantifiable domain name 28 | :type fqdn: string 29 | :param opt: user options 30 | :type opt: Terminal object (for now) 31 | :returns: TestSet object 32 | """ 33 | 34 | def __init__(self, fqdn, opt): 35 | self.testCases = [] 36 | 37 | self.fqdn = fqdn 38 | self.opt = opt 39 | self.info = Information(opt.log, opt.caPathPrefix, opt.testDir, 40 | opt.caPassword, opt.addr, opt.port, opt.kSize) 41 | self.exp = TestExpander(self.fqdn, self.info) 42 | 43 | """ 44 | High level test set build function that determine the flow of the building 45 | process 46 | :returns: TestSet object 47 | """ 48 | 49 | def build(self): 50 | self.opt.log("Checking Root CA...") 51 | self.checkRootCA() 52 | 53 | self.opt.log("Checking Test Directory...") 54 | self.initDirectory() 55 | 56 | if (self.opt.compFunc): 57 | self.opt.log("Building Functionality Test Cases...") 58 | cases = TestFunctionality(self.fqdn, self.info).build().\ 59 | getTestCases() 60 | for test in cases: 61 | self.addTestCase(test, self.opt.replace) 62 | 63 | if (self.opt.compCert): 64 | self.opt.log("Building X509 Test Cases...") 65 | cases = self.getAllTestCases(TestCase) 66 | for test in cases: 67 | self.addTestCase(test, self.opt.replace) 68 | 69 | if (self.opt.compOverflow): 70 | self.opt.log("Building Overflow Test Cases...") 71 | cases = TestOverflow(self.fqdn, self.info, self.opt.overflowLen).\ 72 | build().getTestCases() 73 | for test in cases: 74 | self.addTestCase(test, self.opt.replace) 75 | 76 | self.baseCase = ValidCert(self.fqdn, self.info) 77 | self.baseCase.testBuild(False) 78 | 79 | saveSerial() 80 | 81 | return self 82 | 83 | @staticmethod 84 | def getAllNames(exclude): 85 | tbl = {} 86 | tbl = TestSet.getDescNames(tbl, TestCase, exclude) 87 | tbl = TestFunctionality("get.metadata.only", Information(None)).\ 88 | getAllNames(tbl, exclude) 89 | tbl = TestOverflow("get.metadata.only", Information(None)).\ 90 | getAllNames(tbl, exclude) 91 | return tbl 92 | 93 | @staticmethod 94 | def getDescNames(arr, root, exclude): 95 | for c in root.__subclasses__(): 96 | if (c.__name__ in exclude): 97 | for b in TestSet.getBaseNames({}, c): 98 | if (b.__name__ in arr): 99 | del arr[b.__name__] 100 | else: 101 | arr[c.__name__] = c.__name__ 102 | if (len(c.__subclasses__()) > 0): 103 | TestSet.getDescNames(arr, c, exclude) 104 | 105 | return arr 106 | 107 | @staticmethod 108 | def getBaseNames(arr, base): 109 | if (base != TestCase): 110 | arr[base] = base 111 | TestSet.getBaseNames(arr, base.__bases__[0]) 112 | 113 | return arr 114 | 115 | def getTestSet(self): 116 | return self.testCases 117 | 118 | """ 119 | Initialize the test directory if non existed, or remove all contents in the 120 | directory if 'replace' is asserted 121 | """ 122 | 123 | def initDirectory(self): 124 | if (not os.path.exists(self.info.testDir)): 125 | os.mkdir(self.info.testDir) 126 | if (self.opt.replace): 127 | if (len(os.listdir(self.info.testDir)) != 0 and 128 | not os.path.exists(os.path.join(self.info.testDir, 129 | DEFAULT_METADATA_NAME))): 130 | raise Exception("Please manually remove items in directory %s" 131 | % (self.info.testDir)) 132 | shutil.rmtree(self.info.testDir) 133 | os.mkdir(self.info.testDir) 134 | os.remove(DEFAULT_SERIAL_PATH) 135 | if (not os.path.exists(DEFAULT_SERIAL_PATH)): 136 | with open(DEFAULT_SERIAL_PATH, 'w+') as f: 137 | f.write(str(DEFAULT_SERIAL)) 138 | with open(os.path.join(self.info.testDir, 139 | DEFAULT_METADATA_NAME), 'w+') as f: 140 | f.write('Test Directory') 141 | 142 | 143 | """ 144 | Make sure that the KEY, CRT, and PEM file of the root CA exists 145 | """ 146 | 147 | def checkRootCA(self): 148 | keyFile = self.info.caPathPrefix + ".key" 149 | crtFile = self.info.caPathPrefix + ".crt" 150 | pemFile = self.info.caPathPrefix + ".pem" 151 | dirName = os.path.dirname(self.info.caPathPrefix) 152 | 153 | if (not os.path.exists(dirName)): 154 | os.mkdir(dirName) 155 | 156 | if (not os.path.isfile(keyFile) and not os.path.isfile(crtFile)): 157 | security = CertSec(DEFAULT_CA_NAME) 158 | subject = CertSubj(security.fqdn) 159 | signer = CertSign(None, security.certKey, subject.getSubject()) 160 | cert = Certificate(self.info.caPathPrefix, signer, security) 161 | 162 | cert.addExtension(BasicConstraint(True)) 163 | 164 | cert.security.build() 165 | cert.signer.build() 166 | cert.build() 167 | cert.writeKey(keyPassword=self.info.caPassword) 168 | elif (not os.path.isfile(keyFile) or not os.path.isfile(crtFile)): 169 | raise Exception("Missing %s or %s" % (keyFile, crtFile)) 170 | 171 | if (not os.path.isfile(pemFile)): 172 | shutil.copy(crtFile, pemFile) 173 | 174 | """ 175 | Print and count all test cases in a hierarchical format recursively 176 | :param root: the root of the test hierarchy 177 | :type root: TestCase Class object 178 | :param prepend: visual cue prepended before the name of the test case 179 | :type prepend: string 180 | :returns: integer 181 | """ 182 | 183 | def printAllTestCases(self, root, prepend): 184 | cnt = 0 185 | for c in root.__subclasses__(): 186 | if (self.isExcluded(c)): 187 | continue 188 | 189 | if (len(c.__subclasses__()) > 0): 190 | self.opt.log(prepend + "> " + c.__name__) 191 | cnt += self.printAllTestCases(c, prepend + " ") 192 | else: 193 | case = c("get.metadata.only", self.info) 194 | if (case.isChainable() or case.isAltExtend()): 195 | self.opt.log(prepend + "+ " + case.getTestName()) 196 | if (case.isChainable()): 197 | self.opt.log(prepend + " " + 198 | getChainedName(case.getTestName())) 199 | cnt += 1 200 | if (case.isAltExtend()): 201 | self.opt.log(prepend + " " + 202 | getAltExtendedName(case.getTestName())) 203 | cnt += 1 204 | else: 205 | self.opt.log(prepend + "- " + case.getTestName()) 206 | cnt += 1 207 | return cnt 208 | 209 | """ 210 | Check if the test case is excluded from execution 211 | :param case: the test case in question 212 | :type case: TestCase object or TestCase Class object 213 | :returns: boolean 214 | """ 215 | 216 | def isExcluded(self, case): 217 | c = case 218 | if (not hasattr(c, '__name__')): 219 | c = c.__class__ 220 | 221 | while (True): 222 | name = c.__name__ 223 | 224 | if (name == TestCase.__name__): 225 | break 226 | if (name in self.opt.exclude): 227 | return True 228 | 229 | c = c.__bases__[0] 230 | 231 | return False 232 | 233 | """ 234 | Get the most basic positive test case that all compliance entity should pass 235 | :returns: TestCase object 236 | """ 237 | 238 | def getBaseCase(self): 239 | return self.baseCase 240 | 241 | """ 242 | Insert test cases into an ordered list where the order is based on the 243 | following rules: 244 | 1. critical test cases have highest priority 245 | 2. test cases closer to the root have higher priority 246 | 3. test cases/groups appears first in the file have higher priority 247 | 4. test cases more severe when failed have higher priority 248 | :returns: list of TestCase object 249 | """ 250 | 251 | def getAllTestCases(self, root): 252 | cases = [] 253 | counter = 0 254 | q = queue.PriorityQueue() 255 | 256 | q.put((0, counter, root)) 257 | while (not q.empty()): 258 | c = q.get()[2] 259 | 260 | if (self.isExcluded(c)): 261 | continue 262 | if (not hasattr(c, '__subclasses__')): 263 | cases.append(c) 264 | else: 265 | for r in c.__subclasses__(): 266 | expansions = [] 267 | s = 0 268 | 269 | counter += 1 270 | if (self.exp.isExpansion(r)): 271 | continue 272 | if (len(r.__subclasses__()) != 0): 273 | s += 100000 274 | else: 275 | rc = r 276 | r = rc(self.fqdn, self.info) 277 | if (not r.getCritical()): 278 | if (r.getSeverity() == SEV_HIGH): 279 | s += 100 280 | elif (r.getSeverity() == SEV_MED): 281 | s += 200 282 | else: 283 | s += 300 284 | s += len(rc.__bases__) 285 | 286 | expansions = self.exp.getExpansions(r) 287 | 288 | q.put((s, counter, r)) 289 | for e in expansions: 290 | s += 1 291 | q.put((s, counter, e)) 292 | 293 | counter = 0 294 | for c in cases: 295 | s = 1 296 | counter += 1 297 | if (c.getCritical()): 298 | s = 0 299 | q.put((s, counter, c)) 300 | 301 | cases = [] 302 | while (not q.empty()): 303 | cases.append(q.get()[2]) 304 | 305 | return cases 306 | 307 | def addTestCase(self, testCase, replace=False): 308 | if (VERBOSE): 309 | self.opt.log("building " + str(testCase.getTestName())) 310 | testCase.testBuild(replace) 311 | 312 | if (not (testCase.getTestName() in self.opt.exclude)): 313 | if (VERBOSE): 314 | self.opt.log("adding " + str(testCase.getTestName())) 315 | self.testCases.append(testCase) 316 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yymax/x509test/021787cb597bbe7e17ea5f5755848d5dbe7eeebd/src/__init__.py -------------------------------------------------------------------------------- /x509test.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file contains the entry point for the software. Users should initiate 3 | the program in a terminal by typing "python3 x509test.py" 4 | 5 | @author: Calvin Jia Liang 6 | Created on Sep 3, 2014 7 | """ 8 | 9 | from src.Terminal import * 10 | 11 | if (__name__ == "__main__"): 12 | Terminal().runTest() 13 | --------------------------------------------------------------------------------