├── .gitignore ├── COPYRIGHT.txt ├── GPLv3.txt ├── Makefile ├── README.md ├── bch_code.py ├── bitstring.py ├── bitstringutils.py ├── chipidentify.py ├── license.txt ├── quartus.py ├── randomness.py ├── sigfile.py ├── simulator ├── __init__.py ├── abstractsimulator.py └── ropuf.py ├── spat.bat └── spat.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ 55 | -------------------------------------------------------------------------------- /COPYRIGHT.txt: -------------------------------------------------------------------------------- 1 | Copyright (2014) Sandia Corporation. Under the terms of Contract 2 | DE-AC04-94AL85000, there is a non-exclusive license for use of this 3 | work by or on behalf of the U.S. Government. Export of this program 4 | may require a license from the United States Government. 5 | -------------------------------------------------------------------------------- /GPLv3.txt: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright (2014) Sandia Corporation. Under the terms of Contract 2 | # DE-AC04-94AL85000, there is a non-exclusive license for use of this 3 | # work by or on behalf of the U.S. Government. Export of this program 4 | # may require a license from the United States Government. 5 | 6 | NOTICES := license.txt README.txt COPYRIGHT.txt 7 | 8 | SOURCES := simulator/__init__.py simulator/abstractsimulator.py simulator/ropuf.py bch_code.py bitstring.py bitstringutils.py chipidentify.py spat.py quartus.py randomness.py sigfile.py 9 | 10 | EXTRAS := spat.bat Makefile 11 | 12 | clean: 13 | find . -name "*.py[oc]" -exec rm {} \; 14 | rm *~ *.bak *.swp 15 | .PHONY: clean 16 | 17 | linecount: 18 | wc -l ${SOURCES} 19 | 20 | DIST_NAME := spat-dist 21 | 22 | tgz: ${DIST_NAME}.tar.gz 23 | tar.gz: ${DIST_NAME}.tar.gz 24 | ${DIST_NAME}.tar.gz: ${SOURCES} ${NOTICES} ${EXTRAS} 25 | tar -cvzf ${DIST_NAME}.tar.gz ${SOURCES} ${NOTICES} ${EXTRAS} 26 | 27 | zip: ${DIST_NAME}.zip 28 | ${DIST_NAME}.zip: ${SOURCES} ${NOTICES} ${EXTRAS} 29 | zip ${DIST_NAME}.zip ${SOURCES} ${NOTICES} ${EXTRAS} 30 | 31 | 7z: ${DIST_NAME}.7z 32 | ${DIST_NAME}.7z: ${SOURCES} ${NOTICES} ${EXTRAS} 33 | 7z a ${DIST_NAME}.7z ${SOURCES} ${NOTICES} ${EXTRAS} 34 | 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Sandia National Laboratories PUF Analysis Tool 3 | ============================================== 4 | 5 | Copyright (2014) Sandia Corporation. Under the terms of Contract 6 | DE-AC04-94AL85000, there is a non-exclusive license for use of this 7 | work by or on behalf of the U.S. Government. Export of this program 8 | may require a license from the United States Government. 9 | 10 | This program is free software: you can redistribute it and/or modify 11 | it under the terms of the GNU General Public License as published by 12 | the Free Software Foundation, either version 3 of the License, or 13 | (at your option) any later version. 14 | 15 | This program is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | GNU General Public License for more details. 19 | 20 | You should have received a copy of the GNU General Public License 21 | along with this program. If not, see . 22 | 23 | Introduction 24 | ------------ 25 | 26 | This program is a graphical user interface for measuring and performing inter- 27 | active analysis of physical unclonable functions (PUFs). It is intended for 28 | demonstration and education purposes. See license.txt for license details. 29 | 30 | The program features a PUF visualization that demonstrates how signatures 31 | differ between PUFs and how they exhibit noise over repeated measurements. A 32 | similarity scoreboard shows the user how close the current measurement is to 33 | the closest chip signatures in the database. Other metrics such as average 34 | noise and inter-chip Hamming distances are presented to the user. Randomness 35 | tests published in NIST SP 800-22 can be computed and displayed. Noise and 36 | inter-chip histograms for the sample of PUFs and repeated PUF measurements can 37 | be drawn. 38 | 39 | Application 40 | ----------- 41 | 42 | The program was designed to be used in an educational setting to allow users 43 | to interact with PUFs and analyze their performance. This framework serves as 44 | a step to making PUFs more practical and more broadly understood. 45 | 46 | Requirements 47 | ------------ 48 | 49 | SPAT requires Python 2. It is tested with Python 2.7. Python is freely 50 | available from the Python Software Foundation at: 51 | 52 | www.python.org 53 | 54 | The following packages for Python are required for additional features: 55 | 56 | numpy matplotlib scipy 57 | 58 | These packages are available on most GNU/Linux distributions. Unofficial Windows 59 | binaries for these packages are available from UCI at: 60 | 61 | http://www.lfd.uci.edu/~gohlke/pythonlibs/ 62 | 63 | Otherwise, these packages are available from their respective websites: 64 | 65 | NumPy at www.numpy.org 66 | 67 | matplotlib at matplotlib.org 68 | 69 | SciPy at scipy.org 70 | 71 | Installation 72 | ------------ 73 | 74 | Extract the program files from the ZIP distribution to somewhere you have 75 | read/write access. In a shell or command prompt (Windows), execute the 76 | 'spat.py' file with the Python interpreter: 77 | 78 | python spat.py 79 | 80 | For convenience, a batch file is included for Windows, although this file may 81 | have to be edited if you installed Python 2.7 to a non-standard location. Just 82 | double-click: 83 | 84 | spat.bat 85 | 86 | 87 | Tutorial 88 | ======== 89 | 90 | Once the GUI is up and running, you can begin to experiment with the built-in 91 | PUF simulator. Read the following steps and follow along with the GUI. 92 | 93 | Note that the simulator is the default choice from the Source Select drop-down 94 | menu. With the simulator selected, click the Open button or press the key. 95 | 96 | Choose a virtual chip from the Source Simulator Virtual Chip drop-down menu. 97 | With a real PUF, you would choose one and connect it at this time. 98 | 99 | Click Next or press the space bar to get the first measurement. If this is the 100 | first time that this virtual chip has been measured, a dialog will pop up 101 | asking you to name the device. The program does not associate the virtual chip 102 | selection with the signature metrics and instead asks the user if it is not 103 | sure which chip has been measured. If this were a real chip, you would enter 104 | its serial number. The PUF signature bitmap will update. Note the legend at the 105 | bottom center of the bitmap. The color scheme can also be changed here. 106 | 107 | Note that the simulator parameters are printed at the bottom of the screen 108 | below the legend. P stands for parametric (as in, one of a set of measurable 109 | factors), and E stands for error. In our simulator terminology, a PUF consists 110 | of a set of parametrics that exhibit a level of noise when they are measured. 111 | Hence, measurements of the parametrics are modeled by a distribution which has 112 | a mean value P_mu and a standard deviation P_sd, and noise is added to this 113 | which follows another distribution with mean E_mu and standard deviation E_sd. 114 | Other PUF sources can display other information here on the front panel. 115 | 116 | Continue clicking Next or pressing the space bar to advance the measurement. 117 | Until a significant number of chips are measured a number of times, there may 118 | be some runtime warnings that display in the console. These can be safely 119 | ignored. Note that some bits flip between measurements and that the metrics on 120 | the right-hand side are being updated. The number of total measurements made on 121 | this virtual chip is printed on the bottom-right-hand side. 122 | 123 | Next, choose another virtual chip from the Source Simulator Virtual Chip drop- 124 | down menu. Alternatively, you may select Source -> Simulator -> Random Chip or 125 | press the key to choose one at random. As before, click Next or press the 126 | space bar a few times. There will now be at least two chips on the similarity 127 | scoreboard. Note that the similarity of the current measurement with the 128 | virtual chip that is selected should be near 100% and the similarity with 129 | the other virtual chips should be down near 50%. 130 | 131 | You may continue measuring a few other virtual chips in this fashion or select 132 | Source -> Simulator -> Measure All. The Measure All command will measure each 133 | virtual chip several times so that the sample of chips is fully characterized. 134 | 135 | Next, select Analyze -> Randomness Checks from the menu. This will pop up a new 136 | window that displays a few of the randomness metrics from NIST SP 800-22 "A 137 | Statistical Test Suite for Random and Pseudorandom Number Generators for 138 | Cryptographic Applications". These metrics will be updated whenever a new 139 | measurement is made as long as the Randomness Checks window is open. Please 140 | note that it is normal for some of these checks to fail most of the time for a 141 | given PUF architecture. 142 | 143 | Next, select Analyze -> Draw Histograms from the menu. A new window will pop up 144 | displaying the noise and inter-chip histograms. Each time a measurement is 145 | made, a noise distance and several inter-chip distances are stored. If it is not 146 | the first measurement, a noise distance is stored. If there have been other 147 | chips measured, one inter-chip distance is stored for each other chip that is 148 | known. Unlike the Randomness Checks window, this display will not update when 149 | you click Next. This decision was made to keep the amount of time required to 150 | update the display low. There are several historgram types which can be 151 | selected. These are simple, split and cumulative, which all display the same 152 | information in different ways. The simple option plots the inter-chip and noise 153 | distributions directly. The split option shows the inter-chip and noise 154 | distributions in separate plots that are stacked vertically. The cumulative 155 | option shows the effective cumulative distribution function for the samples of 156 | inter-chip and noise distances. 157 | 158 | Finally, select Analyze -> Save Report if you would like to output all of the 159 | metrics to a file. 160 | 161 | 162 | Description of Commands 163 | ======================= 164 | 165 | All of the controls can be accessed via the "file menu". Some are repeated at 166 | the bottom of the front panel for convenience. The outputs consist of the 167 | signature visualization (the main feature of the GUI), the similarity 168 | scoreboard and other statistics on the right-hand side, the randomness checks 169 | window, the histograms window, the signature log files and statistics files 170 | (XMLs) and the Report. 171 | 172 | Source Submenu 173 | -------------- 174 | 175 | Within the source submenu, you may select the PUF source, open and close the 176 | connection, take a measurement and enable error correction coding (ECC). The 177 | Simulator submenu has functions for facilitating choosing a virtual chip from 178 | the virtual lot. 179 | 180 | Chip DB Submenu 181 | --------------- 182 | 183 | The chip database tracks the names and responses of the PUFs that are measured. 184 | It also tracks things such as a map of unstable bits for each PUF, statistics 185 | such as the number of measurements made for each PUF, and noise and inter-chip 186 | distances. By default, the data is stored in XML format at the following path: 187 | 188 | data/[Source Name]/signatures.xml 189 | 190 | Under the Chip DB menu, you can click Open to load an alternative XML file. 191 | Click save to write the current data to the XML file (this is normally done 192 | upon exit). Click Clear to erase the signatures in the database and all of the 193 | statistics. 194 | 195 | View Submenu 196 | ------------ 197 | 198 | Within the View submenu, options for the front panel can be selected. The PUF 199 | signature bitmap can be scaled. The fonts can also be scaled. These options are 200 | provided for presentation purposes. 201 | 202 | The colormap can be selected under the View menu or on the front panel. The two 203 | color schemes are "grayscale" and "immediate difference". The default color 204 | scheme, "greyscale", represents the average value for each bit, with black 205 | representing 0 and white representing 1. Unstable bits will be shown with a gray 206 | value in between. In the immediate difference scheme, stable 0 bits are shown 207 | with black and stable 1 bits are shown in white. If a bit position has ever 208 | flipped, it is marked unstable, and is shown in red or yellow if it is 209 | currently 0 or 1, respectively. 210 | 211 | The last item in this menu allows the user to disable the probability of 212 | aliasing metric display on the front panel. This display should be disabled 213 | when there are a large number of chips in the signature database. This metric 214 | is computed each time a measurement is made and can make the interface very 215 | slow when using a large number of chips. 216 | 217 | Analyze Submenu 218 | --------------- 219 | 220 | In the Analyze menu, the Randomness checks window can be opened, a number of 221 | histogram types can be plotted, and a report can be generated. 222 | 223 | The Randomness checks are metrics published in NIST SP 800-22 and can help the 224 | user decide if the current PUF response is random or not. Please note that it 225 | is normal for some of these checks to fail most of the time for a given PUF 226 | architecture. 227 | 228 | The histograms help the user decide the PUF signal to noise ratio. The ideal 229 | Hamming distance between two PUF responses is 50% of the bits. The ideal Hamming 230 | distance between any two measurements of the same PUF is zero. The probability 231 | of aliasing is also shown on the histogram plots. This probability is computed 232 | by first fitting the distributions of both the inter-chip and noise Hamming 233 | distances with Gamma distributions. Then, a threshold is chosen that represents 234 | the upper bound of 99.7% of the noise distances. Finally, we evaluate the 235 | Cumulative Distribution Function (CDF) of the inter-chip distances at this noise 236 | threshold. This number represents the probability that two PUFs (chips) will 237 | have responses with Hamming distances less than the level of noise apart. Note 238 | that although this metric can be computed with at least two measurements of a 239 | single PUF and at least two known PUF signatures, it should not be used until a 240 | significant number of measurements have been made. We recommend that many PUFs 241 | be measured (30 or more) and that each PUF is measured several times (30 times 242 | or more). 243 | 244 | The report function allows the user to capture all the information on the front 245 | panel to a file. 246 | 247 | Front Panel 248 | ----------- 249 | 250 | We define the front panel to include all of the widgets on the main window 251 | excluding the File menu. The PUF signature visualization is meant to be the 252 | central focus of the GUI. In this widget, the PUF response bits are split into 253 | sqrt(N) rows and columns, where N is the PUF response length. 254 | 255 | Below the signature visualization is its legend. On the left-hand side, you may 256 | choose the color scheme. The color schemes are described above in the tutorial. 257 | 258 | Along the bottom of the front panel are some controls which are available in the 259 | File menu, but are repeated here for convience. You may choose the PUF source, 260 | open the source, enable error correction coding (ECC), advance the measurement 261 | and disconnect. 262 | 263 | On the right-hand side are all of the PUF metrics computed on the sample of PUFs 264 | which have been measured. At the top is the similarity scoreboard. This shows 265 | the similarity, in % bits, between the current PUF measurement and the closest 266 | of the signatures in the database. Next is the number of flipped bits between 267 | the current measurement and the previous one. This is reported as both a 268 | fraction and a percentage. Next, the number of unstable bits is reported. A bit 269 | map is maintained of unstable bits using the logical OR of the bit map 270 | (initially all zeros) with the XOR of the current measurement and the previous 271 | measurement. Effectively, bits that flip between two consecutive measurements 272 | are forever set in the unstable bit map. Next is the average noise and inter- 273 | chip Hamming distances. The average noise Hamming distance is computed among 274 | all PUFs which have been measured. Each time that a measurement is made, the 275 | number of bits that flipped between the current measurement and the last is 276 | added to the set of noise distances. Also with each measurement, the Hamming 277 | distances between the current signature and the signatures for all other known 278 | PUFs are computed. These are referred to as the inter-chip distances. Finally, 279 | the probability of aliasing is shown. This metric is described above in the 280 | tutorial, and represents the probability that two PUFs like the ones in your 281 | sample will have responses that are within the noise tolerance of one another. 282 | As mentioned above, this metric should be ignored unless a significant number 283 | of PUFs have been measured and a significant number of measurements have been 284 | made on each. 285 | 286 | 287 | About the Simulator 288 | =================== 289 | 290 | The simulator produces signatures by emulating an implementation of a ring 291 | oscillator (RO) PUF. When the simulator is first run, it generates a sample of 292 | virtual chips. For each chip, it generates a collection of RO frequencies. These 293 | frequencies are taken from a normal distribution (random.normalvariate). The 294 | default parameters for this distribution are specified in the simulator.py file. 295 | To generate a binary signature for one of the virtual chips, noise is added to 296 | the RO frequencies which were generated in the previous step. The magnitude of 297 | noise is also a parameter to the simulator. Then, the noisy RO frequencies are 298 | compared to generate binary bits with varying stabilities. 299 | 300 | About Error Correction 301 | ====================== 302 | 303 | Please note that the facilities for performing ECC are included with the GUI, 304 | but the binary executables are not included. These two binaries encode and 305 | decode the signatures using a BCH cyclic error-correcting code. When a measure- 306 | ment is made, the encode utility is used to create the syndrome. This syndrome 307 | is stored in the chip database and can be recalled to correct specific number 308 | of errors in subsequent measurements. 309 | 310 | The source code for the BCH encoder/decoder software from Micron Technology, 311 | Inc. was obtained at: 312 | http://www.codeforge.com/article/136423 313 | 314 | Files 315 | ===== 316 | 317 | The GUI writes files to the following locations. 318 | 319 | simulator_setup.xml Describes a sample of virtual chips 320 | data/[source name]/signatures.xml Name to signature mapping and statistics 321 | data/[source name]/[chip name].dat Binary record of each measurement made 322 | 323 | 324 | Note about Extending 325 | ==================== 326 | 327 | Obviously, the system we have developed won't be a perfect fit for every PUF. 328 | First, there is currently no way to provide a challenge to the PUF. Second, the 329 | user cannot change the PUF size on the GUI. Third, the visualization has only 330 | been tested when the number of PUF bits is a perfect square. In other words, the 331 | number of PUF bits has to be the square of some integer. Provided as a set of 332 | Python modules and scripts, the program was designed to allow the user to look 333 | under the hood and modify the way it works. Hopefully, we will be able to 334 | continue developing the program to suit the needs of most applications. In the 335 | mean time, feel free to modify the program and give us feedback. 336 | 337 | Interfacing with Your Hardware 338 | ------------------------------ 339 | 340 | In quartus.py, an interface is provided for programming an FPGA and 341 | communicating with a script (not provided) which reads signatures from the FPGA. 342 | This script is interactive and provides a hexadecimal-encoded PUF response to 343 | standard output each time the user sends a newline character. It will ignore any 344 | initial data output by the script before the first newline character is input. 345 | After the PUF signature and a newline character, the script is expected to 346 | output a time stamp and a temperature code, separated by a space. The format for 347 | the time stamp is Unix epoch. The format for the temperature is a degrees 348 | Celcius fraction string. An example output follows. 349 | 350 | [user presses return] 351 | e741cc88ca80b9919561fba122c0ef4150a1ee8ce3619d8c42c1bb9981a0f7b04340ebe586a1d988 352 | 4161ab8052e1efa8a5c0ef004b81eca1d3a1b980a661fb8146e0e70189d1ea8453a19d09e761aa80 353 | c6e071a5cdc0e98913b1c2816771a9a0cae1cb0cd3c0ae89a381a9814320daa1c640a58ccce1eb89 354 | 99c0db8972a1afa480 355 | 1362077874 31.69 356 | 357 | The list of sources are configured in 'spat.py' in Application.sourceList and 358 | Application.quartusSources. 359 | 360 | -------------------------------------------------------------------------------- /bch_code.py: -------------------------------------------------------------------------------- 1 | # bch_code.py 2 | # Copyright (2014) Sandia Corporation. Under the terms of Contract 3 | # DE-AC04-94AL85000, there is a non-exclusive license for use of this 4 | # work by or on behalf of the U.S. Government. Export of this program 5 | # may require a license from the United States Government. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program. If not, see . 19 | # 20 | import os, bitstring, random, math 21 | import subprocess 22 | 23 | from bitstringutils import * 24 | 25 | class bch_code(object): 26 | """An error-correcting class""" 27 | 28 | def __init__(self, nb=1024, fileName='bch_code_log.dat'): 29 | self.nb = nb 30 | self.bit_flips = None 31 | self.logFileName = fileName 32 | self.logFile = open(self.logFileName, 'ab') 33 | 34 | def __destroy__(self): 35 | self.close() 36 | 37 | def close(self): 38 | if (not self.logFile.closed): 39 | self.logFile.flush() 40 | self.logFile.close() 41 | 42 | def setup(self, first_measurement, MM=13, TT=20, KK=1024, PP=8): 43 | """A function to enroll the PUF with the error corrector object 44 | 45 | Arguments: 46 | MM : Galois field, GF, for code. Code length is 2^-1. 47 | The default value is 13 for a code length 8191. If the parameter is 48 | set to 0, the program will estimate the value based upon the values 49 | chosen for k and t. 50 | TT : Correction power of the code. Default = 4 51 | KK : Number of data bits to be encoded. Must divide 4. 52 | The default value is the maximum supported by the code which 53 | depends upon the field (-m) and the correction (-t) chosen. 54 | PP : Parallelism in encoder. Does not effect results but 55 | does change the algorithm used to generate them. Default = 8""" 56 | 57 | self.m = MM; self.t = TT; self.k = KK; self.p = PP 58 | p = subprocess.Popen("bch_encoder.exe -m %d -t %d -k %d -p %d" % (self.m, self.t, self.k, self.p), stdin=subprocess.PIPE, stdout=subprocess.PIPE) 59 | p.stdin.write(first_measurement.hex) 60 | output, errors = p.communicate() 61 | codeword, syndrome = output.split() 62 | self.syndrome = syndrome 63 | return self.syndrome 64 | 65 | def decode(self, response): 66 | p = subprocess.Popen("bch_decoder.exe -m %d -t %d -k %d -p %d" % (self.m, self.t, self.k, self.p), stdin=subprocess.PIPE, stdout=subprocess.PIPE) 67 | p.stdin.write(response.hex + self.syndrome + "\n") 68 | p.stdin.flush() 69 | p.stdin.close() 70 | output, errors = p.communicate() 71 | if len (output.strip()) != self.nb / 4: 72 | raise ValueError ("Invalid signature returned from decoder") 73 | 74 | return bitstring.Bits(hex="0x"+output.strip()) 75 | 76 | if __name__=="__main__": 77 | print "Running self-test" 78 | 79 | import simulator 80 | mySim = simulator.Simulator() 81 | mySim.setup() 82 | 83 | firstMeasurement = mySim.next() 84 | print firstMeasurement.hex 85 | myCoder = bch_code() 86 | helper_data = myCoder.setup(firstMeasurement) # setup with defaults 87 | 88 | print "Syndrome: " + myCoder.syndrome 89 | 90 | newMeasurement = mySim.next() 91 | print "(Possibly-) Errored Measurement:\n" + newMeasurement.hex 92 | print "Errors: " + str(hd(firstMeasurement, newMeasurement)) 93 | print "Recovered:\n" + myCoder.decode(newMeasurement).hex 94 | print "Reduced errors: " + str(hd(firstMeasurement, myCoder.decode(newMeasurement))) 95 | 96 | print "Done!" 97 | 98 | -------------------------------------------------------------------------------- /bitstringutils.py: -------------------------------------------------------------------------------- 1 | """ 2 | bitstringutils.py - A few functions for operating on Bitstring objects. 3 | See bitstring.py 4 | """ 5 | 6 | __license__ = """ 7 | GPL Version 3 8 | 9 | Copyright (2014) Sandia Corporation. Under the terms of Contract 10 | DE-AC04-94AL85000, there is a non-exclusive license for use of this 11 | work by or on behalf of the U.S. Government. Export of this program 12 | may require a license from the United States Government. 13 | 14 | This program is free software: you can redistribute it and/or modify 15 | it under the terms of the GNU General Public License as published by 16 | the Free Software Foundation, either version 3 of the License, or 17 | (at your option) any later version. 18 | 19 | This program is distributed in the hope that it will be useful, 20 | but WITHOUT ANY WARRANTY; without even the implied warranty of 21 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 22 | GNU General Public License for more details. 23 | 24 | You should have received a copy of the GNU General Public License 25 | along with this program. If not, see . 26 | """ 27 | 28 | __version__ = "1.2" 29 | 30 | __author__ = "Ryan Helinski and Mitch Martin" 31 | 32 | import bitstring 33 | 34 | def hd (a, b): 35 | return (a ^ b).count(1) 36 | 37 | def hw (a): 38 | return a.count(1) 39 | 40 | def as_ints(bits): 41 | return bits.unpack(fmt='bin:%d' % len(bits))[0] 42 | -------------------------------------------------------------------------------- /chipidentify.py: -------------------------------------------------------------------------------- 1 | """ 2 | chipidentify.py - A database-like object that stores information 3 | about PUF measurements on a sample of chips. This information can 4 | be loaded and stored in an XML format for persistence. Rudimentary 5 | functions are provided to support statistical analysis. 6 | """ 7 | 8 | __license__ = """ 9 | GPL Version 3 10 | 11 | Copyright (2014) Sandia Corporation. Under the terms of Contract 12 | DE-AC04-94AL85000, there is a non-exclusive license for use of this 13 | work by or on behalf of the U.S. Government. Export of this program 14 | may require a license from the United States Government. 15 | 16 | This program is free software: you can redistribute it and/or modify 17 | it under the terms of the GNU General Public License as published by 18 | the Free Software Foundation, either version 3 of the License, or 19 | (at your option) any later version. 20 | 21 | This program is distributed in the hope that it will be useful, 22 | but WITHOUT ANY WARRANTY; without even the implied warranty of 23 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 24 | GNU General Public License for more details. 25 | 26 | You should have received a copy of the GNU General Public License 27 | along with this program. If not, see . 28 | """ 29 | 30 | __version__ = "1.2" 31 | 32 | __author__ = "Ryan Helinski and Mitch Martin" 33 | 34 | import os, subprocess, glob, time 35 | from bitstring import Bits, BitStream 36 | import bitstringutils 37 | import xml.etree.ElementTree as etree 38 | from xml.etree.ElementTree import ParseError 39 | 40 | class ChipIdentify: 41 | """This class is used to identify a chip's name based on a database of PUF signatures""" 42 | 43 | # static variables 44 | max_num_dists = 64 # should be at least 64 in practice 45 | 46 | def __init__(self, fileName = "chipsignatures.xml", nb=1024): 47 | self.nb = nb 48 | self.fileName = fileName 49 | self.setup() 50 | 51 | if (os.path.isfile(self.fileName)): 52 | self.load() 53 | print "Using signature database at '%s' with %d chip signatures" % (self.fileName, len(self)) 54 | else: 55 | print "WARNING: No chip signatures found at '%s'" % self.fileName 56 | if not os.path.isdir(os.path.split(self.fileName)[0]): 57 | os.makedirs(os.path.split(self.fileName)[0]) 58 | 59 | def setup(self): 60 | # To map chip names to signatures: 61 | self.signatureMap = dict() 62 | # For each chip, we also want to have a list of noise and inter-chip distances 63 | self.noiseDistMap = dict() 64 | self.interChipDistMap = dict() 65 | # To keep track of the unstable bit positions 66 | self.unstableBits = dict() 67 | self.measCount = dict() 68 | 69 | def clear(self): 70 | self.setup() 71 | self.save() 72 | 73 | def __len__(self): 74 | return len(self.signatureMap) 75 | 76 | def load(self): 77 | """Load data from file""" 78 | try: 79 | mytree = etree.parse(self.fileName) 80 | except ParseError as pe: 81 | print pe 82 | return 83 | 84 | myroot = mytree.getroot() 85 | 86 | if myroot.tag != 'chip_list': 87 | raise NameError('Expecting this XML file to contain one element as its root') 88 | 89 | for subelement in myroot: 90 | if subelement.tag != 'chip': 91 | raise NameError(' element must contain only elements') 92 | self.measCount[subelement.get('name')] = int(subelement.get('meas_count')) if 'meas_count' in subelement.attrib else 1 93 | for subsub in subelement: 94 | if subsub.tag == 'sig': 95 | if subsub.attrib['encoding'] != 'hex': 96 | raise NameError('Only hex encoding supported, add "encoding=hex" and use a hex string') 97 | self.signatureMap[subelement.get('name')] = Bits("0x"+subsub.text) 98 | 99 | elif subsub.tag == 'noise': 100 | if subelement.get('name') not in self.noiseDistMap: 101 | self.noiseDistMap[subelement.get('name')] = [] 102 | for noise_dist in subsub: 103 | if noise_dist.tag != 'dist': 104 | raise NameError('Tags under must be ') 105 | self.noiseDistMap[subelement.get('name')].append(int(noise_dist.text)) 106 | 107 | elif subsub.tag == 'inter_chip': 108 | if subelement.get('name') not in self.interChipDistMap: 109 | self.interChipDistMap[subelement.get('name')] = dict() 110 | for other_name in subsub: 111 | if other_name.tag != 'other': 112 | raise NameError('Tags under must be ') 113 | if other_name.get('name') not in self.interChipDistMap[subelement.get('name')]: 114 | self.interChipDistMap[subelement.get('name')][other_name.get('name')] = [] 115 | for other_dist in other_name: 116 | if other_dist.tag != 'dist': 117 | raise NameError('Tags under must be ') 118 | self.interChipDistMap[subelement.get('name')][other_name.get('name')].append(int(other_dist.text)) 119 | 120 | elif subsub.tag == 'unstable_bits': 121 | if subsub.attrib['encoding'] != 'hex': 122 | raise NameError('Only hex encoding supported, add "encoding=hex" and use a hex string') 123 | self.unstableBits[subelement.get('name')] = Bits("0x"+subsub.text) 124 | 125 | else: 126 | raise NameError('Unsupported tag %s' % subsub.tag) 127 | 128 | def save(self, altFileName=None): 129 | if altFileName != None: 130 | self.fileName = altFileName 131 | chipListEl = etree.Element('chip_list') 132 | chipListEl.text = "\n\t" 133 | for name, sig in sorted(self.signatureMap.items(), key=lambda item: item[0] ): 134 | chipEl = etree.SubElement(chipListEl, 'chip', attrib={'name':name, 'meas_count':str(self.measCount[name])}) 135 | chipEl.text = "\n" + 2*"\t" 136 | chipEl.tail = "\n" + "\t" 137 | sigEl = etree.SubElement(chipEl, 'sig', attrib={'encoding':'hex'}) 138 | sigEl.text = sig.hex 139 | sigEl.tail = "\n" + 2*"\t" 140 | if name in self.noiseDistMap: 141 | noiseListEl = etree.SubElement(chipEl, 'noise') 142 | noiseListEl.tail = "\n" + 2*"\t" 143 | for dist in self.noiseDistMap[name]: 144 | noiseEl = etree.SubElement(noiseListEl, 'dist') 145 | noiseEl.text = str(dist) 146 | if name in self.interChipDistMap: 147 | interListEl = etree.SubElement(chipEl, 'inter_chip') 148 | interListEl.text = "\n" + 3*"\t" 149 | interListEl.tail = "\n" + 2*"\t" 150 | for other_name, dist_list in sorted(self.interChipDistMap[name].items(), key=lambda item: item[0] ): 151 | otherNameEl = etree.SubElement(interListEl, 'other') 152 | otherNameEl.attrib['name'] = other_name 153 | otherNameEl.tail = "\n" + 3*"\t" 154 | for dist in dist_list: 155 | interEl = etree.SubElement(otherNameEl, 'dist') 156 | interEl.text = str(dist) 157 | otherNameEl.tail = "\n" + 2*"\t" 158 | 159 | if name in self.unstableBits: 160 | unstableBitsEl = etree.SubElement(chipEl, 'unstable_bits', attrib={'encoding':'hex'}) 161 | unstableBitsEl.text = self.unstableBits[name].hex 162 | unstableBitsEl.tail = "\n" + "\t" 163 | 164 | chipEl.tail = "\n" 165 | 166 | print 'Saving chip signature database to \'%s\'' % self.fileName 167 | xmlfile = open(self.fileName, 'w') 168 | #xml_extras.indent(chipListEl) # add white space to XML DOM to result in pretty printed string 169 | xmlfile.write('\n' + etree.tostring(chipListEl)) 170 | xmlfile.flush() 171 | xmlfile.close() # don't need to sync because we close here 172 | 173 | def Identify(self, bits): 174 | "This compares a bit string against all known chip signatures and returns the closest match" 175 | 176 | hd_dict = self.MatchMap(bits) 177 | return min(hd_dict.items(), key=lambda item: item[1]) 178 | 179 | def MatchMap(self, bits): 180 | "This compares a bit string against all known chip signatures" 181 | 182 | hd_dict = dict() 183 | for key, value in self.signatureMap.items(): 184 | relhd = float(bitstringutils.hd(bits, value))/self.nb 185 | hd_dict[key] = relhd 186 | 187 | return hd_dict 188 | 189 | def add(self, chip_name, sig): 190 | # I can store more than one per in the XML and do averaging, 191 | # but since I'm using the minimum Hamming distance, there's no problem with 192 | # just storing the first measured signature here 193 | self.signatureMap[chip_name] = sig 194 | self.measCount[chip_name] = 0 195 | 196 | def get_sig(self, chip_name): 197 | return self.signatureMap[chip_name] 198 | 199 | def process_sig (self, chip_name, sig): 200 | """This computes and records some greedy statistics on a given signature""" 201 | 202 | # add this chip if it is unknown 203 | if chip_name not in self.signatureMap.keys(): 204 | self.add(chip_name, sig) 205 | else: 206 | # update unstable bit map 207 | if chip_name not in self.unstableBits: 208 | self.unstableBits[chip_name] = BitStream(uint=0, length=self.nb) 209 | if self.measCount[chip_name] > 0: 210 | self.unstableBits[chip_name] = self.unstableBits[chip_name] | (self.signatureMap[chip_name] ^ sig) 211 | 212 | # Increment the measurement count for this chip 213 | self.measCount[chip_name] += 1 214 | 215 | # record 1 noise distance 216 | if chip_name not in self.noiseDistMap.keys(): 217 | self.noiseDistMap[chip_name] = [] 218 | else: 219 | # assume that if we didn't have a list, that this is the first measurement, 220 | # and therefore we need to wait for a subsequent one before we can compute a noise distance 221 | self.noiseDistMap[chip_name].append(bitstringutils.hd(sig, self.signatureMap[chip_name])) 222 | # for scalability, should truncate this list 223 | if len(self.noiseDistMap[chip_name]) > self.max_num_dists: 224 | self.noiseDistMap[chip_name] = self.noiseDistMap[chip_name][-self.max_num_dists:] 225 | 226 | # and record (N_C - 1) inter-chip distances 227 | for other_chip_name in self.signatureMap.keys(): 228 | if other_chip_name == chip_name: 229 | # don't compare to self 230 | continue 231 | if chip_name not in self.interChipDistMap.keys(): 232 | self.interChipDistMap[chip_name] = dict() 233 | if other_chip_name not in self.interChipDistMap[chip_name].keys(): 234 | self.interChipDistMap[chip_name][other_chip_name] = [] 235 | self.interChipDistMap[chip_name][other_chip_name].append( 236 | bitstringutils.hd(sig, self.signatureMap[other_chip_name])) 237 | # for scalability, I truncate this list 238 | if len(self.interChipDistMap[chip_name][other_chip_name]) > self.max_num_dists: 239 | self.interChipDistMap[chip_name][other_chip_name] = self.interChipDistMap[chip_name][other_chip_name][-self.max_num_dists:] 240 | 241 | def get_meas_count(self, chip_name): 242 | if chip_name in self.measCount: 243 | return self.measCount[chip_name] 244 | else: 245 | return 0 246 | 247 | def get_num_unstable_bits (self, chip_name): 248 | return self.unstableBits[chip_name].count(1) 249 | 250 | def unstable_bits_valid (self, chip_name): 251 | return self.measCount[chip_name] > 1 252 | 253 | def get_noise_dist_avg (self, chip_name): 254 | return float(sum(self.noiseDistMap[chip_name]))/max(1,len(self.noiseDistMap[chip_name])) 255 | 256 | def get_inter_dist_avg (self, chip_name): 257 | inter_dists = [sum(inter_dist_list) for inter_dist_list in self.interChipDistMap[chip_name].values()] 258 | num_dists = [len(inter_dist_list) for inter_dist_list in self.interChipDistMap[chip_name].values()] 259 | return float(sum(inter_dists))/max(1, sum(num_dists)) 260 | 261 | def get_all_noise_dists (self): 262 | all_noise_dists = [] 263 | for chip_name, noise_dists in self.noiseDistMap.items(): 264 | all_noise_dists.extend(noise_dists) 265 | return all_noise_dists 266 | 267 | def get_all_inter_chip_dists (self): 268 | all_inter_chip_dists = [] 269 | for this_chip_name, dist_map in self.interChipDistMap.items(): 270 | for other_chip_name, inter_chip_dists in dist_map.items(): 271 | all_inter_chip_dists.extend(inter_chip_dists) 272 | return all_inter_chip_dists 273 | 274 | def prob_alias(self, plot=False): 275 | """Returns tuple (threshold, probability)""" 276 | 277 | from scipy.stats import gamma 278 | # scipy-ref.pdf Section 5.13 on page 390 279 | 280 | if plot: 281 | import matplotlib.pyplot as plt 282 | plt.ion() 283 | plt.clf() 284 | 285 | nd = self.get_all_noise_dists() 286 | a, loc, scale = gamma.fit(nd) 287 | ndrv = gamma(a, loc, scale) 288 | if plot: 289 | plt.hist(nd, normed=True) # 'normed' might become 'density' later? 290 | x = range(max(nd)) 291 | plt.plot(x, ndrv.pdf(x)) 292 | 293 | icd = self.get_all_inter_chip_dists() 294 | a, loc, scale = gamma.fit(icd) 295 | icdrv = gamma(a, loc, scale) 296 | if plot: 297 | plt.hist(icd, normed=True) 298 | x = range(max(icd)) 299 | plt.plot(x, icdrv.pdf(x)) 300 | 301 | # Here it goes! 302 | threshold = ndrv.ppf(0.997) 303 | if plot: 304 | plt.axvline(threshold) 305 | prob = icdrv.cdf(threshold) 306 | print 'Noise 99.7%% threshold: %f, probability of aliasing: %1.3e' % (threshold, prob) 307 | return threshold, prob 308 | 309 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | GPL Version 3 2 | 3 | Copyright (2014) Sandia Corporation. Under the terms of Contract 4 | DE-AC04-94AL85000, there is a non-exclusive license for use of this 5 | work by or on behalf of the U.S. Government. Export of this program may require a license from the United States Government. 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | -------------------------------------------------------------------------------- /quartus.py: -------------------------------------------------------------------------------- 1 | """ 2 | quartus.py - A class for interfacing with Altera's Quartus software 3 | and communicating with a PUF over the Virtual JTAG Interface (VJI). 4 | """ 5 | 6 | __license__ = """ 7 | GPL Version 3 8 | 9 | Copyright (2014) Sandia Corporation. Under the terms of Contract 10 | DE-AC04-94AL85000, there is a non-exclusive license for use of this 11 | work by or on behalf of the U.S. Government. Export of this program 12 | may require a license from the United States Government. 13 | 14 | This program is free software: you can redistribute it and/or modify 15 | it under the terms of the GNU General Public License as published by 16 | the Free Software Foundation, either version 3 of the License, or 17 | (at your option) any later version. 18 | 19 | This program is distributed in the hope that it will be useful, 20 | but WITHOUT ANY WARRANTY; without even the implied warranty of 21 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 22 | GNU General Public License for more details. 23 | 24 | You should have received a copy of the GNU General Public License 25 | along with this program. If not, see . 26 | """ 27 | 28 | __version__ = "1.2" 29 | 30 | __author__ = "Ryan Helinski and Mitch Martin" 31 | 32 | import os, subprocess, glob, time 33 | import bitstring 34 | from bitstringutils import * 35 | 36 | quartus_path = "C:\\altera\\10.1sp1\\quartus\\bin" 37 | 38 | quartus_pgm = quartus_path + "\quartus_pgm" 39 | quartus_stp = quartus_path + "\quartus_stp" 40 | 41 | def try_syscall (cmd, max_tries=32): 42 | num_tries = 0 43 | 44 | while (num_tries < max_tries): 45 | print "> " + cmd 46 | retval = os.system(cmd) 47 | if (retval == 0): 48 | break 49 | else: 50 | "Error returned, re-trying" 51 | num_tries += 1 52 | time.sleep(1) 53 | 54 | if (num_tries > max_tries): 55 | print "Gave up trying" 56 | else: 57 | print "Command returned OK" 58 | 59 | return retval 60 | 61 | def conv_temp_f (degs_c): 62 | return float(degs_c) * 9 / 5 + 32 63 | 64 | class QuartusCon(object): 65 | "A class to connect to an FPGA via Quartus" 66 | 67 | def __init__(self, nb=1024, tclFile="measureARBR.tcl", cdf_filename='BeMicroII_schem.cdf'): 68 | self.nb = nb 69 | self.tclFile = tclFile 70 | self.cdf_filename = cdf_filename 71 | 72 | def __destroy__(self): 73 | self.close() 74 | 75 | def close(self): 76 | if (self.subProcess.returncode == None): 77 | try: 78 | self.subProcess.stdin.write("q\n") 79 | self.subProcess.stdin.flush() 80 | self.subProcess.stdin.close() 81 | if self.subProcess.wait() != 0: 82 | print "Subprocess did not return 0" 83 | except IOError as e: 84 | print "Failed to close gracefully" 85 | 86 | def program(self): 87 | retval = try_syscall("%s --cable=\"USB-Blaster\" --mode=\"JTAG\" --operation=p %s" % (quartus_pgm, self.cdf_filename)) 88 | 89 | if (retval == 0): 90 | stp_args = [quartus_stp, '-t', self.tclFile] 91 | print "Pipe> " + " ".join(stp_args) 92 | self.subProcess = subprocess.Popen(stp_args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 93 | for i in range(2): 94 | print self.subProcess.stdout.readline() 95 | 96 | return retval 97 | 98 | def next(self): 99 | self.subProcess.stdin.write("\n") 100 | self.subProcess.stdin.flush() 101 | for i in range(1): 102 | print self.subProcess.stdout.readline() 103 | myline = self.subProcess.stdout.readline().strip() 104 | print repr(myline) 105 | new_bits = bitstring.Bits(hex='0x' + myline[0:(self.nb/4)]) 106 | 107 | (self.time, self.temp) = self.subProcess.stdout.readline().split(" ") 108 | 109 | return new_bits 110 | 111 | def get_temp(self, format="C"): 112 | try: 113 | if (format=="F"): 114 | return conv_temp_f(self.temp) 115 | else: 116 | return float(self.temp) 117 | except (AttributeError): 118 | return "" 119 | 120 | -------------------------------------------------------------------------------- /randomness.py: -------------------------------------------------------------------------------- 1 | """ 2 | randomness.py - A collection of functions that read a BitString and 3 | compute randomness metrics. These functions were selected for their 4 | ability to have simple implementations in hardware. 5 | 6 | These functions are derived from NIST Special Publication 800-22: 7 | http://csrc.nist.gov/publications/nistpubs/800-22-rev1a/SP800-22rev1a.pdf 8 | """ 9 | 10 | __license__ = """ 11 | GPL Version 3 12 | 13 | Copyright (2014) Sandia Corporation. Under the terms of Contract 14 | DE-AC04-94AL85000, there is a non-exclusive license for use of this 15 | work by or on behalf of the U.S. Government. Export of this program 16 | may require a license from the United States Government. 17 | 18 | This program is free software: you can redistribute it and/or modify 19 | it under the terms of the GNU General Public License as published by 20 | the Free Software Foundation, either version 3 of the License, or 21 | (at your option) any later version. 22 | 23 | This program is distributed in the hope that it will be useful, 24 | but WITHOUT ANY WARRANTY; without even the implied warranty of 25 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 26 | GNU General Public License for more details. 27 | 28 | You should have received a copy of the GNU General Public License 29 | along with this program. If not, see . 30 | """ 31 | 32 | __version__ = "1.2" 33 | 34 | __author__ = "Ryan Helinski and Mitch Martin" 35 | 36 | import bitstring 37 | import math # so that I can math 38 | from numpy import cumsum, array 39 | from scipy.stats import norm 40 | 41 | def entropy (bits, min=False, p_value=0.01): 42 | """Implements entropy and min. entropy 43 | returns a tuple representing (metric, pass)""" 44 | 45 | P = float(bits.count(1)) / len(bits) 46 | 47 | if min: 48 | H_X = - math.log( max( [P, 1-P] ) ) 49 | else: 50 | H_X = - P * math.log(P, 2) - (1-P) * math.log(1-P, 2) 51 | 52 | return H_X, H_X >= (1 - p_value) 53 | 54 | def min_entropy (bits, p_value=0.67): 55 | """Minimum entropy convenience function 56 | returns a tuple representing (metric, pass)""" 57 | return entropy(bits, min=True, p_value=p_value) 58 | 59 | def monobit (bits, p_value=0.01): 60 | """Monobit Test 61 | returns a tuple representing (metric, pass)""" 62 | S = bits.count(1) - bits.count(0) 63 | S_obs = abs(S) / math.sqrt(len(bits)) 64 | P = math.erfc(S_obs / math.sqrt(2)) 65 | return P, P >= p_value 66 | 67 | def runs_test (bits, p_value=0.01): 68 | """Runs Test 69 | returns a tuple representing (metric, pass)""" 70 | n = len(bits) 71 | pi = float(bits.count(1)) / n 72 | tau = 2.0 / math.sqrt(n) 73 | if abs(pi - 0.5) >= tau: 74 | return 0, False 75 | vobs = (bits[0:n-1] ^ bits[1:n]).count(1) + 1 76 | pval = math.erfc(abs(vobs-2*n*pi*(1-pi)) / (2 * math.sqrt(2*n) * pi * (1 - pi))) 77 | return pval, pval >= p_value 78 | 79 | def runs_test2 (bits, p_value=0.01): 80 | """Custom Runs Test (pi = 0.5) 81 | returns a tuple representing (metric, pass)""" 82 | n = len(bits) 83 | pi = .5 84 | vobs = (bits[0:n-1] ^ bits[1:n]).count(1) + 1 85 | pval = math.erfc(abs(vobs-2*n*pi*(1-pi)) / (2 * math.sqrt(2*n) * pi * (1 - pi))) 86 | return pval, pval >= p_value 87 | 88 | def cum_sum (bits, p_value=0.01): 89 | """Cumulative Sums Test 90 | returns a tuple representing (metric, pass)""" 91 | n = len(bits) 92 | X = array([int(bits[x]) for x in range(n)]) 93 | X = X * 2 - 1 94 | cs = cumsum(X) 95 | z = max(abs(cs)) 96 | lim_upper = int(((float(n) / z) - 1) / 4) 97 | lim_lower1 = int(((-float(n) / z) + 1) / 4) 98 | lim_lower2 = int(((-float(n) / z) - 3) / 4) 99 | 100 | k = array(range(lim_lower1, lim_upper+1), float) 101 | sum1 = (norm.cdf(((4*k + 1) * z) / math.sqrt(n)) - \ 102 | norm.cdf(((4*k - 1) * z) / math.sqrt(n)) ).sum() 103 | k = array(range(lim_lower2, lim_upper+1), float) 104 | sum2 = (norm.cdf(((4*k + 3) * z) / math.sqrt(n)) - \ 105 | norm.cdf(((4*k + 1) * z) / math.sqrt(n)) ).sum() 106 | pval = 1 - sum1 + sum2 107 | 108 | return pval, pval > p_value 109 | 110 | def rel_diff (a, b): 111 | """Convenience function for computing relative difference""" 112 | return (a - b) / b 113 | 114 | def rel_equal (a, b, tol=10e-6): 115 | """Convenience function for deciding if two floats are practically equal""" 116 | return rel_diff(a, b) < tol 117 | 118 | def randBitString(length): 119 | import random 120 | return bitstring.BitString(uint=random.getrandbits(length), length=length) 121 | 122 | if __name__ == '__main__': 123 | print "Testing Randomness functions..." 124 | import random, itertools 125 | 126 | # For timing the execution 127 | from datetime import datetime 128 | startTime = datetime.now() 129 | 130 | numBits = 2**10 131 | numTrials = 2**12 132 | parallel = True 133 | 134 | SP800_22_examples = [ 135 | # page 2-3 (PDF 25) 136 | { 'fun':monobit, 137 | 'input':bitstring.BitString(bin='1011010101'), 138 | 'output':0.527089}, 139 | { 'fun':monobit, 140 | 'input':bitstring.BitString(bin='1100100100001111110110101010001000100001011010001100001000110100110001001100011001100010100010111000'), 141 | 'output':0.109599}, 142 | # page 2-33 (PDF 55) 143 | { 'fun':cum_sum, 144 | 'input':bitstring.BitString(bin='1011010111'), 145 | 'output':0.4116588}, 146 | { 'fun':cum_sum, 147 | 'input':bitstring.BitString(bin='1100100100001111110110101010001000100001011010001100001000110100110001001100011001100010100010111000'), 148 | 'output':0.219194}, 149 | # page 2-7 (PDF 29) 150 | { 'fun':runs_test, 151 | 'input':bitstring.BitString(bin='1001101011'), 152 | 'output':0.147232}, 153 | { 'fun':runs_test, 154 | 'input':bitstring.BitString(bin='1100100100001111110110101010001000100001011010001100001000110100110001001100011001100010100010111000'), 155 | 'output':0.500798}, 156 | ] 157 | 158 | print "SP800-22rev1a Examples:" 159 | for example in SP800_22_examples: 160 | our_output = example['fun'](example['input'])[0] 161 | print "%20s: from pub: %10f, ours: %10f" % ( 162 | example['fun'].__name__, 163 | example['output'], 164 | our_output) + \ 165 | ", match: " + ("OK" if rel_equal(example['output'], our_output) else "FAIL") 166 | 167 | if parallel: 168 | from multiprocessing import Pool, cpu_count 169 | 170 | print "Number of bits in each string: %d, number of trials: %d" % (numBits, numTrials) 171 | if parallel: 172 | pool = Pool(processes=cpu_count()) 173 | randBitStrings = pool.map(randBitString, itertools.repeat(numBits, numTrials)) 174 | else: 175 | randBitStrings = map(randBitString, itertools.repeat(numBits, numTrials)) 176 | 177 | for randomness_fun in [entropy, 178 | min_entropy, 179 | monobit, 180 | runs_test, 181 | runs_test2, 182 | cum_sum]: 183 | 184 | if parallel: 185 | results = pool.map(randomness_fun, randBitStrings) 186 | else: 187 | results = map(randomness_fun, randBitStrings) 188 | 189 | avg_p_value = sum([result[0] for result in results]) / numTrials 190 | avg_pass = float(sum([result[1] for result in results])) / numTrials 191 | print "%20s: Average p-value: %f, pass: %f" % (randomness_fun.__name__, avg_p_value, avg_pass) 192 | 193 | print (datetime.now() - startTime) 194 | -------------------------------------------------------------------------------- /sigfile.py: -------------------------------------------------------------------------------- 1 | """ 2 | sigfile.py - A class for loading and storing PUF signatures to files. 3 | Multiple file formats could be implemented here. 4 | """ 5 | 6 | __license__ = """ 7 | GPL Version 3 8 | 9 | Copyright (2014) Sandia Corporation. Under the terms of Contract 10 | DE-AC04-94AL85000, there is a non-exclusive license for use of this 11 | work by or on behalf of the U.S. Government. Export of this program 12 | may require a license from the United States Government. 13 | 14 | This program is free software: you can redistribute it and/or modify 15 | it under the terms of the GNU General Public License as published by 16 | the Free Software Foundation, either version 3 of the License, or 17 | (at your option) any later version. 18 | 19 | This program is distributed in the hope that it will be useful, 20 | but WITHOUT ANY WARRANTY; without even the implied warranty of 21 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 22 | GNU General Public License for more details. 23 | 24 | You should have received a copy of the GNU General Public License 25 | along with this program. If not, see . 26 | """ 27 | 28 | __version__ = "1.2" 29 | 30 | __author__ = "Ryan Helinski and Mitch Martin" 31 | 32 | import os 33 | import bitstring 34 | from bitstringutils import * 35 | 36 | class SigFile(object): 37 | """A class to load and store signatures in various formats""" 38 | 39 | def __init__(self, fileName, nb=1024): 40 | self.nb = nb 41 | self.fileName = fileName 42 | #self.open() 43 | 44 | def __destroy__(self): 45 | self.close() 46 | 47 | def close(self): 48 | try: 49 | self.f.close() 50 | except AttributeError as e: 51 | return False 52 | return True 53 | 54 | def open(self, fileName=None, mode='rb'): 55 | if (fileName): 56 | self.fileName = fileName 57 | 58 | if not os.path.isdir(os.path.split(self.fileName)[0]): 59 | os.makedirs(os.path.split(self.fileName)[0]) 60 | self.f = open(self.fileName, mode) 61 | 62 | def next(self): 63 | if ('f' not in self.__dict__ or not self.f.mode.startswith('r')): 64 | self.open() 65 | bindata = self.f.read(self.nb/8) 66 | if (len(bindata) < self.nb/8): 67 | # Start back at the beginning 68 | print "Hit EOF, starting back at the beginning" 69 | self.f.seek(0) 70 | bindata = self.f.read(self.nb/8) 71 | 72 | new_bits = bitstring.Bits(bytes=bindata) 73 | #print repr(new_bits) 74 | 75 | return new_bits 76 | 77 | def append(self, new_bits): 78 | # should check if file is open 79 | #if (self.f.closed or not self.f.mode.startswith('a')): 80 | #self.f.open 81 | if ('f' in self.__dict__ and not self.f.closed): 82 | self.f.close() 83 | 84 | self.open(mode='ab') 85 | 86 | self.f.write(new_bits.bytes) 87 | self.f.flush() 88 | os.fsync(self.f) # make sure it gets written now 89 | 90 | def __getitem__(self, index): 91 | offset = index * self.nb / 8 92 | self.f.seek(offset) 93 | return self.next() 94 | 95 | # Not sure supporting __setitem__ makes sense 96 | 97 | def save(self, fileName): 98 | if fileName.endswith('.dat'): 99 | self.bits.tofile(fileName) 100 | 101 | -------------------------------------------------------------------------------- /simulator/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandialabs/spat/4a300f211a527cef091250d8a3bad1fed68af9b2/simulator/__init__.py -------------------------------------------------------------------------------- /simulator/abstractsimulator.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python 2 | """ 3 | abstractsimulator.py - A class that simulates a sample of PUFs. 4 | This file serves as an abstract type from which actual simulator 5 | classes can be defined. Please note that it does NOT work as-is. 6 | 7 | This program features a self-test that is executed when this file 8 | is executed, rather than being imported. 9 | """ 10 | 11 | __license__ = """ 12 | GPL Version 3 13 | 14 | Copyright (2014) Sandia Corporation. Under the terms of Contract 15 | DE-AC04-94AL85000, there is a non-exclusive license for use of this 16 | work by or on behalf of the U.S. Government. Export of this program 17 | may require a license from the United States Government. 18 | 19 | This program is free software: you can redistribute it and/or modify 20 | it under the terms of the GNU General Public License as published by 21 | the Free Software Foundation, either version 3 of the License, or 22 | (at your option) any later version. 23 | 24 | This program is distributed in the hope that it will be useful, 25 | but WITHOUT ANY WARRANTY; without even the implied warranty of 26 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 27 | GNU General Public License for more details. 28 | 29 | You should have received a copy of the GNU General Public License 30 | along with this program. If not, see . 31 | """ 32 | 33 | __version__ = "1.2" 34 | 35 | __author__ = "Ryan Helinski and Mitch Martin" 36 | 37 | import os, bitstring, random 38 | from bitstringutils import * 39 | import xml.etree.ElementTree as etree 40 | 41 | class AbstractSimulator(object): 42 | """An abstract class for PUF simulators.""" 43 | 44 | def __init__(self, nb=1024): 45 | self.nb = nb 46 | self.bit_flips = None 47 | self.setupFile = 'data/Simulator/simulator_setup.xml' 48 | if (not os.path.isdir(os.path.dirname(self.setupFile))): 49 | os.makedirs(os.path.dirname(self.setupFile)) 50 | 51 | def setup(self, param_mu=10, param_sd=0.00001, noise_mu=0, noise_sd=0.025, numVirtChips=32): 52 | self.params = {'param_mu':param_mu, 'param_sd':param_sd, 'noise_mu':noise_mu, 'noise_sd':noise_sd} 53 | if (os.path.isfile(self.setupFile)): 54 | self.loadFromFile() 55 | else: 56 | self.generateSetup() 57 | print "Done." 58 | 59 | def loadFromFile(self): 60 | print "Loading simulator state... ", 61 | mytree = etree.parse(self.setupFile) 62 | myroot = mytree.getroot() 63 | 64 | self.realValues = [] 65 | self.chipNames = [] 66 | for child in myroot: 67 | if (child.tag == 'setup'): 68 | self.params = dict(zip(child.attrib.keys(), [float(val) for val in child.attrib.values()])) 69 | elif (child.tag == 'virtchip'): 70 | self.chipNames.append(child.attrib['name']) 71 | for chipchild in child: 72 | if (chipchild.tag == 'realvalues'): 73 | chipRealValues = [] 74 | for value in chipchild: 75 | chipRealValues.append(float(value.text.strip())) 76 | self.realValues.append(chipRealValues) 77 | self.numVirtChips = len(self.realValues) 78 | self.numElements = len(self.realValues[0]) 79 | 80 | def generateSetup(self): 81 | raise NotImplemented() 82 | 83 | def close(self): 84 | """For compatibility with other bit sources""" 85 | return True 86 | 87 | def getChipName(self, index): 88 | return 'v%03d' % (index+1) 89 | 90 | def makeSigFile (self, sigFile='simulator_sigs.xml'): 91 | chipListEl = etree.Element('chip_list') 92 | chipListEl.text = "\n" 93 | chipListEl.tail = "\n" 94 | for index in range(self.numVirtChips): 95 | chipEl = etree.SubElement(chipListEl, 'chip', attrib={'name':self.getChipName(index)}) 96 | chipEl.text = "\n" 97 | chipEl.tail = "\n" 98 | sigEl = etree.SubElement(chipEl, 'sig', attrib={'encoding':'hex'}) 99 | sigEl.text = self.next(index).hex 100 | sigEl.tail = "\n" 101 | 102 | xmlparent = os.path.split(sigFile)[0] 103 | if not os.path.isdir(xmlparent): 104 | os.makedirs(xmlparent) 105 | xmlfile = open(sigFile, 'w') 106 | xmlfile.write('\n' + etree.tostring(chipListEl)) 107 | xmlfile.flush() 108 | xmlfile.close() 109 | 110 | def getSetupStr(self): 111 | return "P_mu=%1.1f, P_sd=%1.1f, E_mu=%1.3f, E_sd=%1.3f" % ( 112 | self.params['param_mu'], self.params['param_sd'], self.params['noise_mu'], self.params['noise_sd']) 113 | 114 | def noise(self): 115 | return random.normalvariate(self.params['noise_mu'], self.params['noise_sd']) 116 | 117 | def next(self, virtChipIndex=0): 118 | raise NotImplemented() 119 | 120 | def characterize(self, chipIdentifier, numMeas=32): 121 | import ttk, Tkinter 122 | dlg = Tkinter.Toplevel() 123 | dlg.title("Simulator Progress") 124 | l = Tkinter.Label(dlg, text="Measuring each chip %d times" % numMeas) 125 | l.pack() 126 | w = ttk.Progressbar(dlg, maximum=self.numVirtChips) 127 | w.pack() 128 | for ci in range(self.numVirtChips): 129 | print 'Measuring chip # %d %d times' % (ci, numMeas) 130 | for ri in range(numMeas): 131 | sig = self.next(ci) 132 | chipIdentifier.process_sig(self.getChipName(ci), sig) 133 | w.step() 134 | dlg.update() 135 | w.stop() 136 | dlg.destroy() 137 | 138 | 139 | def NoiseWorker(argTuple): 140 | """Measure one of the chips multiple times. For use with multiprocessor.pool """ 141 | 142 | chipIndex, iterations = argTuple 143 | # Instead of generating the number of iterations for each process, I could create my own iterator object and pass that in as the argument 144 | mySim = AbstractSimulator() 145 | mySim.setup() 146 | enrollment = mySim.next(chipIndex) 147 | noise_hds = [hd(enrollment, mySim.next(chipIndex)) for measIndex in range(iterations)] 148 | print "Chip v%03d (of %d): %d / %d = %0.3f %%" % (chipIndex+1, mySim.numVirtChips, sum(noise_hds), iterations * mySim.nb, (100 * float(sum(noise_hds)) / iterations / mySim.nb)) 149 | return float(sum(noise_hds)) / iterations / mySim.nb 150 | 151 | # A self-test routine that characterizes the population statistics resulting from the setup parameters 152 | if __name__=="__main__": 153 | import multiprocessing, itertools 154 | print "Running self-test" 155 | mySim = AbstractSimulator() 156 | mySim.setup() # setup with defaults 157 | p = multiprocessing.Pool(multiprocessing.cpu_count()) 158 | argIter = itertools.izip(range(mySim.numVirtChips), itertools.repeat(2 ** 6)) 159 | results = p.map(NoiseWorker, argIter) 160 | 161 | print "Average noise Hamming distance: %f" % (sum(results) / mySim.numVirtChips) 162 | print "Test done" 163 | 164 | -------------------------------------------------------------------------------- /simulator/ropuf.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python 2 | """ 3 | simulator.py - A class that simulates a sample of PUFs. Currently, the Ring 4 | Oscillator PUF is modeled. Mean oscillator frequencies are first created for a 5 | sample of virtual chips. Then, the RO PUF model is used to create PUF signatures 6 | with realistic noise. The virtual chip sample parameters are saved to an XML 7 | file so the same sample of chips can be used later. 8 | 9 | The architecture is an implementation of a system published by G. Suh and S. 10 | Devadas, "Physical unclonable functions for device authentication and secret key 11 | generation", in Proc. DAC'07, pp. 9-14, 2007. 12 | 13 | This program features a self-test that is executed when this file 14 | is executed, rather than being imported. 15 | """ 16 | 17 | __license__ = """ 18 | GPL Version 3 19 | 20 | Copyright (2014) Sandia Corporation. Under the terms of Contract 21 | DE-AC04-94AL85000, there is a non-exclusive license for use of this 22 | work by or on behalf of the U.S. Government. Export of this program 23 | may require a license from the United States Government. 24 | 25 | This program is free software: you can redistribute it and/or modify 26 | it under the terms of the GNU General Public License as published by 27 | the Free Software Foundation, either version 3 of the License, or 28 | (at your option) any later version. 29 | 30 | This program is distributed in the hope that it will be useful, 31 | but WITHOUT ANY WARRANTY; without even the implied warranty of 32 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 33 | GNU General Public License for more details. 34 | 35 | You should have received a copy of the GNU General Public License 36 | along with this program. If not, see . 37 | """ 38 | 39 | __version__ = "1.2" 40 | 41 | __author__ = "Ryan Helinski and Mitch Martin" 42 | 43 | import os, random 44 | import bitstring 45 | from bitstringutils import * 46 | import xml.etree.ElementTree as etree 47 | from abstractsimulator import AbstractSimulator 48 | 49 | class Simulator(AbstractSimulator): 50 | """A PUF-simulating class. Produces simulated PUF responses.""" 51 | 52 | def setup(self, param_mu=10, param_sd=1, noise_mu=0, noise_sd=0.0225, numVirtChips=2 ** 5): 53 | """Generate the real values to which noise is added and the PUF architecture is modelled to produce binary responses. The default parameters have been selected to create an inter-chip response Hamming distance of 50% of the number of bits and a noise Hamming distance of 1%. The front panel performs poorly when the number of virtual chips is large. """ 54 | 55 | self.params = {'param_mu':param_mu, 'param_sd':param_sd, 'noise_mu':noise_mu, 'noise_sd':noise_sd} 56 | self.numVirtChips = numVirtChips 57 | if (os.path.isfile(self.setupFile)): 58 | self.loadFromFile() 59 | else: 60 | self.generateSetup() 61 | 62 | print "Done." 63 | 64 | def generateSetup(self): 65 | print "Generating virtual chips...", 66 | self.numElements = self.nb + 1 67 | self.realValues = [[random.normalvariate(self.params['param_mu'], self.params['param_sd']) for index in range(self.numElements)] for chip in range(self.numVirtChips)] 68 | self.chipNames = [('v%03d' % (index + 1)) for index in range(self.numVirtChips)] 69 | 70 | myxml = etree.Element('xml', attrib={'version':'1.0', 'encoding':'UTF-8'}) 71 | myxml.text = "\n" 72 | setupEl = etree.SubElement(myxml, 'setup', attrib=dict(zip(self.params.keys(), [str(val) for val in self.params.values()]))) 73 | setupEl.tail = "\n" 74 | for index in range(self.numVirtChips): 75 | virtChipEl = etree.SubElement(myxml, 'virtchip', attrib={'name':self.chipNames[index]}) 76 | virtChipEl.text = "\n" 77 | virtChipEl.tail = "\n" 78 | valsEl = etree.SubElement(virtChipEl, 'realvalues') 79 | valsEl.text = "\n" 80 | valsEl.tail = "\n" 81 | for param in self.realValues[index]: 82 | child = etree.SubElement(valsEl, 'value') 83 | child.text = str(param) 84 | child.tail = "\n" 85 | 86 | xmlfile = open(self.setupFile, 'w') 87 | xmlfile.write(etree.tostring(myxml)) 88 | xmlfile.flush() 89 | xmlfile.close() 90 | 91 | 92 | def next(self, virtChipIndex=0): 93 | if type(virtChipIndex) == str: 94 | virtChipIndex = int(virtChipIndex[1:4]) - 1 95 | bits = bitstring.BitArray() 96 | 97 | noiseValues = [self.noise() for i in range(self.nb+1)] 98 | # This is the linear RO PUF architecture which avoids redundant 99 | # bits which are inherent in the all possible combinations approach 100 | # i.e., comparisons a ? b and b ? c may render a ? c redundant 101 | # Instead, we use (NB + 1) ring oscillators and only compare adjacent 102 | # oscillators (i) and (i+1) for i in (0, NB). 103 | for i in range(0, self.nb): 104 | lhs = self.realValues[virtChipIndex][i] + noiseValues[i] 105 | rhs = self.realValues[virtChipIndex][i+1] + noiseValues[i+1] 106 | bits.append(bitstring.Bits(bool=(lhs < rhs))) 107 | 108 | return bits 109 | 110 | def NoiseWorker(argTuple): 111 | """Measure one of the chips multiple times. For use with multiprocessor.pool """ 112 | 113 | chipIndex, iterations = argTuple 114 | # Instead of generating the number of iterations for each process, I could create my own iterator object and pass that in as the argument 115 | mySim = Simulator() 116 | mySim.setup() 117 | enrollment = mySim.next(chipIndex) 118 | noise_hds = [hd(enrollment, mySim.next(chipIndex)) for measIndex in range(iterations)] 119 | print "Chip v%03d (of %d): %d / %d = %0.3f %%" % (chipIndex+1, mySim.numVirtChips, sum(noise_hds), iterations * mySim.nb, (100 * float(sum(noise_hds)) / iterations / mySim.nb)) 120 | return float(sum(noise_hds)) / iterations / mySim.nb 121 | 122 | # A self-test routine that characterizes the population statistics 123 | # resulting from the setup parameters 124 | # 125 | # NOTE: When run this way, you MUST include the parent directory in 126 | # the PYTHONPATH environmental variable. For example: 127 | # export PYTHONPATH = ".." 128 | # 129 | if __name__=="__main__": 130 | import multiprocessing, itertools 131 | print "Running self-test" 132 | mySim = Simulator() 133 | mySim.setup() # setup with defaults 134 | p = multiprocessing.Pool(multiprocessing.cpu_count()) 135 | argIter = itertools.izip(range(mySim.numVirtChips), itertools.repeat(2 ** 6)) 136 | results = p.map(NoiseWorker, argIter) 137 | 138 | print "Average noise Hamming distance: %f" % (sum(results) / mySim.numVirtChips) 139 | print "Test done" 140 | 141 | -------------------------------------------------------------------------------- /spat.bat: -------------------------------------------------------------------------------- 1 | REM This is an example batch file which may be used to start the program 2 | 3 | REM Copyright (2014) Sandia Corporation. Under the terms of Contract 4 | REM DE-AC04-94AL85000, there is a non-exclusive license for use of this 5 | REM work by or on behalf of the U.S. Government. Export of this program 6 | REM may require a license from the United States Government. 7 | 8 | start "SPAT Console" python spat.py 9 | -------------------------------------------------------------------------------- /spat.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python 2 | 3 | """ 4 | spat.py - A Python TkInter GUI for visually measuring and 5 | demonstrating physical uncloneable functions 6 | """ 7 | 8 | __license__ = """ 9 | GPL Version 3 10 | 11 | Copyright (2014) Sandia Corporation. Under the terms of Contract 12 | DE-AC04-94AL85000, there is a non-exclusive license for use of this 13 | work by or on behalf of the U.S. Government. Export of this program 14 | may require a license from the United States Government. 15 | 16 | This program is free software: you can redistribute it and/or modify 17 | it under the terms of the GNU General Public License as published by 18 | the Free Software Foundation, either version 3 of the License, or 19 | (at your option) any later version. 20 | 21 | This program is distributed in the hope that it will be useful, 22 | but WITHOUT ANY WARRANTY; without even the implied warranty of 23 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 24 | GNU General Public License for more details. 25 | 26 | You should have received a copy of the GNU General Public License 27 | along with this program. If not, see . 28 | """ 29 | 30 | __version__ = "1.2" 31 | 32 | __author__ = "Ryan Helinski and Mitch Martin" 33 | 34 | __copyright__ = """ 35 | Copyright (2014) Sandia Corporation. Under the terms of Contract 36 | DE-AC04-94AL85000, there is a non-exclusive license for use of this 37 | work by or on behalf of the U.S. Government. Export of this program 38 | may require a license from the United States Government. 39 | """ 40 | 41 | __credits__ = ["Ryan Helinski", "Mitch Martin", "Jason Hamlet", "Todd Bauer", "Bijan Fakhri"] 42 | 43 | from Tkinter import * 44 | import tkFont 45 | import tkFileDialog 46 | import tkSimpleDialog 47 | import tkMessageBox 48 | import math 49 | import os 50 | import time 51 | from collections import OrderedDict 52 | 53 | # Local packages 54 | from sigfile import * 55 | from quartus import * 56 | from chipidentify import * 57 | import bitstring 58 | from simulator.ropuf import * 59 | import bch_code 60 | import randomness 61 | 62 | def fmtFractionPercent(num, den): 63 | return '%d / %d = %.3f%%' % (num, den, 100*(float(num) / den)) 64 | 65 | class Application(Frame): 66 | # static variables 67 | numMatchScores = 8 68 | colorMapImmDiff = OrderedDict( [('00', '#000000'), 69 | ('10', '#ffffff'), 70 | ('01', '#ff0000'), 71 | ('11', '#ffff00')] ) 72 | colorMapGray = OrderedDict( [('0%', '#000000'), 73 | ('50%', '#808080'), 74 | ('100%', '#ffffff')] ) 75 | sourceList = ('Simulator', 'File', 'ROPUF', 'ARBR') 76 | quartusSources = { 77 | 'ROPUF' : {'tclFile' : 'measureROPUF.tcl', 78 | 'cdf_filename' : 'BeMicroII_ROPUF.cdf'}, 79 | 'ARBR' : {'tclFile' : 'measureARBR.tcl', 80 | 'cdf_filename' : 'BeMicroII_ARBR_Controller.cdf'} 81 | } 82 | colorMapList = ('Grayscale', 'Imm. Diff.') 83 | distHistTypeList = ('Simple', 'Split', 'Cumulative') 84 | maxAvgDepth = 32 85 | noiseThreshold = 0.25 86 | randomnessFunMap = OrderedDict([ 87 | ('Entropy', randomness.entropy), 88 | ('Min. Entropy', randomness.min_entropy), 89 | ('Monobit', randomness.monobit), 90 | ('Runs Test', randomness.runs_test), 91 | ('Runs Test 2', randomness.runs_test2), 92 | ('Cumul. Sums', randomness.cum_sum) 93 | ]) 94 | outputPath = 'data' 95 | 96 | 97 | def __init__(self, master=None): 98 | self.nb = 1024 99 | self.chipNum = 0 100 | self.bitFlips = None 101 | self.bits = bitstring.BitStream(uint=0, length=self.nb) 102 | self.reset() 103 | self.bigfont = tkFont.Font(family="Helvetica", size=12) 104 | self.font = tkFont.Font(family="Helvetica", size=10) 105 | self.squareSize = int(math.sqrt(self.nb)) 106 | self.zoomFactor = int(480/self.squareSize) 107 | self.colorMapFun = self.mapBitGrayscale 108 | Frame.__init__(self, master) 109 | master.protocol("WM_DELETE_WINDOW", self._delete_window) 110 | self.statusStr = 'Not Connected' 111 | self.updateTitle() 112 | self.grid() 113 | self.createWidgets() 114 | 115 | def reset(self): 116 | self.measurementCounter = 0 117 | self.bitFlips = None 118 | self.bitAvgs = [bitstring.BitArray() for x in range(self.nb)] 119 | 120 | def updateTitle(self, statusStr=None): 121 | if statusStr: 122 | self.statusStr = statusStr 123 | self.master.title(" - ".join(["PUF Visual Interface", self.statusStr])) 124 | 125 | def _delete_window(self): 126 | print 'Caught delete_window event' 127 | self.save() 128 | self.master.destroy() 129 | 130 | def _destroy(self, event=None): 131 | print 'Caught destroy event' 132 | if event: 133 | print event 134 | self.save() 135 | 136 | def quit(self, event=None): 137 | self.save() 138 | self.master.quit() 139 | self.destroy() 140 | 141 | def save(self, event=None): 142 | if 'chipIdentifier' in self.__dict__: 143 | self.chipIdentifier.save() 144 | 145 | def mapBitImmDiff(self, index): 146 | # TODO performance could be improved 147 | return self.colorMapImmDiff[ str(int(self.bits[index])) + str(int(self.chipIdentifier.unstableBits[self.lastRead][index])) ] 148 | 149 | def mapBitGrayscale(self, index): 150 | return '#' + ('%02x' % (255*float(hw(self.bitAvgs[index]))/max(1, len(self.bitAvgs[index]))) * 3) 151 | 152 | def destroyLegend(self): 153 | self.colorMapLegend.destroy() 154 | self.colorMapLabels = [] 155 | self.colorMapIcons = [] 156 | 157 | def buildColorMapLegend(self, colorMap): 158 | if 'colorMapLegend' in self.__dict__: 159 | self.destroyLegend() 160 | self.colorMapLegend = Frame(self.colorMapFrame) 161 | 162 | # Create the legend 163 | for i in range(len(colorMap)): 164 | code = colorMap.keys()[i] 165 | color = colorMap[code] 166 | self.colorMapIcons.append(Canvas(self.colorMapLegend, width=self.zoomFactor, height=self.zoomFactor, bd=2, relief="groove", bg=color)) 167 | self.colorMapIcons[i].grid(row=0, column=(2*i)+1) 168 | self.colorMapLabels.append(Label(self.colorMapLegend, text='%s %s' % ( 169 | 'Stable' if (code[1] == '0') else 'Unstable', 170 | code[0] 171 | ), font=self.font)) 172 | self.colorMapLabels[i].grid(row=0, column=(2*i)+2) 173 | self.colorMapLegend.grid(row=0, column=1) 174 | 175 | def buildGrayScaleLegend(self): 176 | if 'colorMapLegend' in self.__dict__: 177 | self.destroyLegend() 178 | self.colorMapLegend = Frame(self.colorMapFrame) 179 | 180 | # Create the legend 181 | for i in range(len(self.colorMapGray)): 182 | code = self.colorMapGray.keys()[i] 183 | color = self.colorMapGray[code] 184 | self.colorMapIcons.append(Canvas(self.colorMapLegend, width=self.zoomFactor, height=self.zoomFactor, bd=2, relief="groove", bg=color)) 185 | self.colorMapIcons[i].grid(row=0, column=(2*i)+1) 186 | self.colorMapLabels.append(Label(self.colorMapLegend, text=code, font=self.font)) 187 | self.colorMapLabels[i].grid(row=0, column=(2*i)+2) 188 | self.colorMapLegend.grid(row=0, column=1) 189 | 190 | def createWidgets(self): 191 | self.master.option_add('*tearOff', FALSE) 192 | # Menu Bar 193 | self.menuBar = Menu(self) 194 | # Source Menu 195 | self.menuBarSource = Menu(self.menuBar) 196 | 197 | self.sourceSelected = StringVar() 198 | self.sourceSelected.set(self.sourceList[0]) 199 | self.sourceSelected.trace('w', self.onModeSelect) 200 | self.menuBarSourceSelect = Menu(self.menuBarSource) 201 | for i, source in enumerate(self.sourceList): 202 | self.menuBarSourceSelect.add_radiobutton(label=source, variable=self.sourceSelected, value=source) 203 | self.menuBarSource.add_cascade(label="Select", menu=self.menuBarSourceSelect) 204 | self.menuBarSource.add_command(label="Open", command=self.open, accelerator="O") 205 | self.bind_all("", self.open) 206 | 207 | # For choosing a virtual chip from the virtual lot 208 | self.virtChipNumVar = StringVar() 209 | self.menuBarSourceSimulator = Menu(self.menuBarSource) 210 | self.menuBarSourceSimulatorSelect = Menu(self.menuBarSourceSimulator) 211 | self.menuBarSourceSimulator.add_cascade(label='Virtual Chip', menu=self.menuBarSourceSimulatorSelect) 212 | self.menuBarSourceSimulator.add_command(label='Random Chip', command=self.simulatorPickRandom, accelerator='R') 213 | self.bind_all('', self.simulatorPickRandom) 214 | 215 | self.menuBarSourceSimulator.add_command(label='Measure All', command=self.simulatorMeasureAll) 216 | self.menuBarSource.add_cascade(label='Simulator', menu=self.menuBarSourceSimulator) 217 | self.menuBarSource.entryconfig('Simulator', state=DISABLED) 218 | 219 | self.correctVar = IntVar() 220 | self.menuBarSource.add_checkbutton(label='ECC', variable=self.correctVar, command=self.selectECC) 221 | 222 | self.menuBarSource.add_command(label='Next', command=self.next, accelerator="Space") 223 | self.menuBarSource.entryconfig('Next', state=DISABLED) 224 | self.bind_all("", self.next) 225 | 226 | self.menuBarSource.add_command(label='Disconnect', command=self.close) 227 | self.menuBarSource.entryconfig('Disconnect', state=DISABLED) 228 | self.menuBarSource.add_separator() 229 | self.menuBarSource.add_command(label="Quit", command=self.quit, accelerator="Ctrl+Q") 230 | self.bind_all("", self.quit) 231 | self.menuBar.add_cascade(label="Source", menu=self.menuBarSource) 232 | 233 | # Chip DB Menu 234 | self.menuBarChipDB = Menu(self.menuBar) 235 | self.menuBarChipDB.add_command(label="Open", command=self.loadSigFile, accelerator="Ctrl+O") 236 | self.bind_all("", self.loadSigFile) 237 | self.menuBarChipDB.add_command(label="Save", command=self.save, accelerator="Ctrl+S") 238 | self.bind_all("", self.save) 239 | self.menuBarChipDB.add_command(label="Clear", command=self.clearSigFile, accelerator="Ctrl+N") 240 | self.menuBarChipDB.entryconfig('Clear', state=DISABLED) 241 | self.bind_all("", self.clearSigFile) 242 | 243 | self.menuBar.add_cascade(label="Chip DB", menu=self.menuBarChipDB) 244 | 245 | # View Menu 246 | self.menuBarView = Menu(self.menuBar) 247 | self.menuBarView.add_command(label='Scale Bitmap', command=self.setScale) 248 | self.menuBarView.add_command(label='Font Size', command=self.setFontSize) 249 | 250 | self.colorMapSelected = StringVar() 251 | self.colorMapSelected.set(self.colorMapList[0]) 252 | self.menuBarLegend = Menu(self.menuBarView) 253 | for colorMap in self.colorMapList: 254 | self.menuBarLegend.add_radiobutton(label=colorMap, variable=self.colorMapSelected, value=colorMap) 255 | self.menuBarView.add_cascade(label='Color Map', menu=self.menuBarLegend) 256 | self.menuBar.add_cascade(label='View', menu=self.menuBarView) 257 | 258 | # Allow the user to disable the probability of aliasing statistic on the front panel 259 | self.probAliasEnVar = IntVar() 260 | self.probAliasEnVar.set(1) 261 | self.menuBarView.add_checkbutton(label='Prob. Alias', variable=self.probAliasEnVar) 262 | 263 | # Analyze Menu 264 | self.menuBarAnalyze = Menu(self.menuBar) 265 | self.menuBarAnalyze.add_command(label='Randomness Checks', command=self.runRandomnessCheck) 266 | self.menuBarAnalyze.entryconfig('Randomness Checks', state=DISABLED) 267 | self.menuBarAnalyze.add_separator() 268 | self.menuBarAnalyzeHistogram = Menu(self.menuBarAnalyze) 269 | self.distHistSelected = StringVar() 270 | self.distHistSelected.set(self.distHistTypeList[0]) 271 | for distHist in self.distHistTypeList: 272 | self.menuBarAnalyzeHistogram.add_radiobutton(label=distHist, variable=self.distHistSelected, value=distHist) 273 | self.menuBarAnalyzeHistogram.add_separator() 274 | self.distHistFractions = IntVar() 275 | self.distHistFractions.set(1) 276 | self.menuBarAnalyzeHistogram.add_checkbutton(label='Relative Distances', variable=self.distHistFractions) 277 | self.menuBarAnalyze.add_cascade(label='Histogram Type', menu=self.menuBarAnalyzeHistogram) 278 | self.menuBarAnalyze.add_command(label='Draw Histograms', command=self.runDistHist) 279 | self.menuBarAnalyze.entryconfig('Draw Histograms', state=DISABLED) 280 | self.menuBarAnalyze.add_separator() 281 | self.menuBarAnalyze.add_command(label='Save Report', command=self.writeReport) 282 | self.menuBarAnalyze.entryconfig('Save Report', state=DISABLED) 283 | self.menuBar.add_cascade(label='Analyze', menu=self.menuBarAnalyze) 284 | 285 | # display the menu bar 286 | root.config(menu=self.menuBar) 287 | 288 | # Main Frame 289 | self.sigVis = self.make_pi() 290 | self.sigCanvas = Canvas(self, width=self.squareSize*self.zoomFactor, 291 | height=self.squareSize*self.zoomFactor) 292 | self.sigCanvas.grid(row=0, column=0) 293 | self.sigCanvas.create_image(0, 0, image=self.sigVis, anchor=NW) 294 | 295 | # Legend for color map 296 | self.colorMapFrame = Frame(self) 297 | self.colorMapFrame.grid(row=2, column=0) 298 | self.colorMapLabels = [] 299 | self.colorMapIcons = [] 300 | # add a select menu for color map style 301 | self.colorMapPicker = OptionMenu(self.colorMapFrame, self.colorMapSelected, *self.colorMapList) 302 | self.colorMapPicker.grid(row=0, column=0) 303 | self.colorMapSelected.trace('w', self.onColorMapSelect) 304 | # add the actual legend 305 | self.buildGrayScaleLegend() 306 | 307 | self.tempVar = StringVar() 308 | self.tempLabel = Label (self, textvariable=self.tempVar, font=self.font) 309 | self.tempLabel.grid(row=3, column=0) 310 | 311 | # Group of buttons (PUF Type, Open, ECC, Next, Disconnect, Quit) 312 | self.buttonFrame = Frame(self) 313 | self.buttonFrame.grid(row=6, column=0, columnspan=1) 314 | bpad = 4 315 | 316 | self.sourceSelect = OptionMenu(self.buttonFrame, self.sourceSelected, *self.sourceList) 317 | self.sourceSelect.grid(row=0, column=0) 318 | self.oldMode = self.sourceSelected.get() # for detecting a change in the value 319 | 320 | self.programButton = Button (self.buttonFrame, text='Open', command=self.open) 321 | self.programButton.grid(row=0, column=1, padx=bpad, pady=bpad) 322 | 323 | self.correctButton = Checkbutton(self.buttonFrame, text='ECC', variable=self.correctVar, command=self.selectECC) 324 | self.correctButton.grid(row=0, column=3, padx=bpad, pady=bpad) 325 | 326 | self.nextButton = Button (self.buttonFrame, text='Next', command=self.next) 327 | self.nextButton.grid(row=0, column=4, padx=bpad, pady=bpad) 328 | self.nextButton.config(state=DISABLED) 329 | 330 | self.closeButton = Button (self.buttonFrame, text='Disconnect', command=self.close) 331 | self.closeButton.grid(row=0, column=5, padx=bpad, pady=bpad) 332 | self.closeButton.config(state=DISABLED) 333 | 334 | # Score board 335 | self.scoreFrame = Frame(self) 336 | self.scoreFrame.grid(row=0, column=1, rowspan=3, sticky=N+W) 337 | self.matchHeadingLabel = Label(self.scoreFrame, text='Similarity (% Bits):', font=self.bigfont) 338 | self.matchHeadingLabel.grid(row=0, column=0, columnspan=2, sticky='W') 339 | self.matchLabelVars = [] 340 | self.matchLabels = [] 341 | self.matchScoreLabelVars = [] 342 | self.matchScoreLabels = [] 343 | for i in range(self.numMatchScores): 344 | self.matchLabelVars.append(StringVar()) 345 | self.matchLabels.append(Label (self.scoreFrame, textvariable=self.matchLabelVars[i], font=self.font)) 346 | self.matchLabels[i].grid(row=i+1, column=0, sticky='N') 347 | self.matchScoreLabelVars.append(StringVar()) 348 | self.matchScoreLabels.append(Label (self.scoreFrame, textvariable=self.matchScoreLabelVars[i], font=self.font)) 349 | self.matchScoreLabels[i].grid(row=i+1, column=1, sticky='NE') 350 | 351 | 352 | 353 | # Bit Buffer Statistics 354 | self.bitFlipVar = StringVar() 355 | self.bitFlipLabel = Label(self.scoreFrame, text='Number of flipped bits:', font=self.bigfont) 356 | self.bitFlipLabel.grid(row=self.numMatchScores+1, column=0, sticky='W', padx=bpad, pady=bpad, columnspan=2) 357 | self.bitFlipLabelVar = Label(self.scoreFrame, textvariable=self.bitFlipVar, font=self.font) 358 | self.bitFlipLabelVar.grid(row=self.numMatchScores+2, column=0, sticky='E', padx=bpad, pady=bpad, columnspan=2) 359 | 360 | self.unstableBitVar = StringVar() 361 | self.unstableBitLabel = Label(self.scoreFrame, text='Number of unstable bits:', font=self.bigfont) 362 | self.unstableBitLabel.grid(row=self.numMatchScores+3, column=0, sticky='W', padx=bpad, pady=bpad, columnspan=2) 363 | self.unstableBitLabelVar = Label(self.scoreFrame, textvariable=self.unstableBitVar, font=self.font) 364 | self.unstableBitLabelVar.grid(row=self.numMatchScores+4, column=0, sticky='E', padx=bpad, pady=bpad, columnspan=2) 365 | 366 | 367 | # Chip and chip sample statistics 368 | self.noiseDistVar = StringVar() 369 | self.noiseDistLabel = Label(self.scoreFrame, text='Avg. Noise HD:', font=self.bigfont) 370 | self.noiseDistLabel.grid(row=self.numMatchScores+5, column=0, sticky='W', padx=bpad, pady=bpad, columnspan=2) 371 | self.noiseDistLabelVar = Label(self.scoreFrame, textvariable=self.noiseDistVar, font=self.font) 372 | self.noiseDistLabelVar.grid(row=self.numMatchScores+6, column=0, sticky='E', padx=bpad, pady=bpad, columnspan=2) 373 | 374 | self.interChipDistVar = StringVar() 375 | self.interChipDistLabel = Label(self.scoreFrame, text='Avg. Inter-Chip HD:', font=self.bigfont) 376 | self.interChipDistLabel.grid(row=self.numMatchScores+7, column=0, sticky='W', padx=bpad, pady=bpad, columnspan=2) 377 | self.interChipDistLabelVar = Label(self.scoreFrame, textvariable=self.interChipDistVar, font=self.font) 378 | self.interChipDistLabelVar.grid(row=self.numMatchScores+8, column=0, sticky='E', padx=bpad, pady=bpad, columnspan=2) 379 | 380 | self.probAliasingVar = StringVar() 381 | self.probAliasingLabel = Label(self.scoreFrame, text='Probability of Alias:', font=self.bigfont) 382 | self.probAliasingLabel.grid(row=self.numMatchScores+9, column=0, sticky='W', padx=bpad, pady=bpad, columnspan=2) 383 | self.probAliasingLabelVar = Label(self.scoreFrame, textvariable=self.probAliasingVar, font=self.font) 384 | self.probAliasingLabelVar.grid(row=self.numMatchScores+10, column=0, sticky='E', padx=bpad, pady=bpad, columnspan=2) 385 | 386 | # Print measurement iterator at bottom 387 | self.measNumVar = StringVar() 388 | self.measNumLabel = Label(self, textvariable=self.measNumVar, font=self.font) 389 | self.measNumLabel.grid(row=6, column=1, padx=bpad, pady=bpad) 390 | 391 | def updateMenuBarSimulate(self): 392 | for chipName in self.bitSource.chipNames: 393 | self.menuBarSourceSimulatorSelect.add_radiobutton(label=chipName, variable=self.virtChipNumVar, value=chipName) 394 | 395 | def simulatorMeasureAll(self, event=None): 396 | self.bitSource.characterize(self.chipIdentifier) 397 | 398 | def simulatorPickRandom(self, event=None): 399 | if 'bitSource' in self.__dict__ and type(self.bitSource) == type(Simulator()): 400 | import random 401 | self.virtChipNumVar.set("v%03d" % random.randint(1, len(self.bitSource.chipNames))) 402 | 403 | def selectECC(self): 404 | print "ECC: " + ("Off" if self.correctVar.get() == 0 else "On") 405 | 406 | def onModeSelect(self, *args): 407 | if self.sourceSelected.get() != self.oldMode: 408 | self.close() 409 | self.oldMode = self.sourceSelected.get() 410 | self.updateChipPicker() 411 | 412 | def onColorMapSelect(self, *args): 413 | if self.colorMapSelected.get() == 'Imm. Diff.': 414 | self.colorMapFun = self.mapBitImmDiff 415 | self.buildColorMapLegend(self.colorMapImmDiff) 416 | else: 417 | self.colorMapFun = self.mapBitGrayscale 418 | self.buildGrayScaleLegend() 419 | self.updateWidgets() 420 | 421 | def make_pi(self, bits=None): 422 | "Rebuild the PhotoImage from the PUF signature" 423 | 424 | sigVis = PhotoImage(width=self.squareSize, height=self.squareSize) 425 | 426 | row = 0; col = 0 427 | for i in range(0, self.nb): 428 | sigVis.put(self.colorMapFun(i), (row, col)) 429 | col +=1 430 | if col == self.squareSize: 431 | row += 1; col = 0 432 | 433 | sigVis = sigVis.zoom(self.zoomFactor,self.zoomFactor) 434 | 435 | return sigVis 436 | 437 | def updateBitAvgs(self): 438 | """Remember the current signature so that average bit values can be calculated""" 439 | for i in range(self.nb): 440 | self.bitAvgs[i].append(bitstring.BitArray(bool=self.bits[i])) 441 | if len(self.bitAvgs[i]) > self.maxAvgDepth: 442 | # truncate to last 'n' bits 443 | self.bitAvgs[i] = self.bitAvgs[i][-self.maxAvgDepth:] 444 | 445 | def setSigVis (self): 446 | self.sigVis = self.make_pi() 447 | 448 | def updateStatus(self): 449 | self.statusStr = ( 450 | 'Using Simulator' if self.sourceSelected.get() == 'Simulator' else 451 | 'Reading from File "%s"' % os.path.basename(self.bitSource.fileName) if self.sourceSelected.get() == 'File' else 452 | 'Connected to ROPUF' if self.sourceSelected.get() == 'ROPUF' else 453 | 'Connected to ARBR' if self.sourceSelected.get() == 'ARBR' else 454 | 'Not Connected' 455 | ) 456 | self.updateTitle() 457 | print self.statusStr 458 | 459 | def updateWidgets(self): 460 | scores = sorted(self.chipIdentifier.MatchMap(self.bits).items(), key=lambda item: item[1])[0:self.numMatchScores] 461 | self.updateStatus() 462 | 463 | # Show matches on GUI 464 | for i in range(self.numMatchScores): 465 | if (i < len(scores)): 466 | self.matchLabelVars[i].set(scores[i][0]) 467 | self.matchScoreLabelVars[i].set('%0.2f %%' % (100-scores[i][1]*100)) 468 | else: 469 | # Clear out unused slots 470 | self.matchLabelVars[i].set('') 471 | self.matchScoreLabelVars[i].set('') 472 | 473 | self.tempVar.set( 474 | ("Board Temperature: %0.2f deg. C, %0.2f deg. F" % \ 475 | (self.bitSource.get_temp(), self.bitSource.get_temp("F")) ) \ 476 | if ('bitSource' in self.__dict__ and type(self.bitSource) == type(QuartusCon())) else \ 477 | ('%s' % self.bitSource.getSetupStr()) \ 478 | if ('bitSource' in self.__dict__ and type(self.bitSource) == type(Simulator())) else \ 479 | "") 480 | 481 | self.setSigVis() 482 | self.sigCanvas.create_image(0, 0, image=self.sigVis, anchor=NW) 483 | 484 | if(self.measurementCounter>0): 485 | self.bitFlipVar.set(fmtFractionPercent(self.bitFlips, self.nb)) 486 | else: 487 | self.bitFlipVar.set('') 488 | 489 | if(self.chipIdentifier.unstable_bits_valid(self.lastRead)): 490 | self.unstableBitVar.set(fmtFractionPercent(self.chipIdentifier.get_num_unstable_bits(self.lastRead), self.nb)) 491 | self.noiseDistVar.set(fmtFractionPercent(self.chipIdentifier.get_noise_dist_avg(self.lastRead), self.nb)) 492 | else : 493 | self.unstableBitVar.set('') 494 | self.noiseDistVar.set('') 495 | 496 | if (self.lastRead in self.chipIdentifier.interChipDistMap): 497 | self.interChipDistVar.set(fmtFractionPercent(self.chipIdentifier.get_inter_dist_avg(self.lastRead), self.nb)) 498 | else: 499 | self.interChipDistVar.set('') 500 | 501 | 502 | if self.probAliasEnVar.get() and self.chipIdentifier.get_meas_count(self.lastRead) > 2 and len(self.chipIdentifier) > 2: 503 | self.probAliasingVar.set( "%.1e" % (self.chipIdentifier.prob_alias()[1]) ) 504 | else: 505 | self.probAliasingVar.set( "N/A" ) 506 | 507 | self.measNumVar.set("Meas. #: %d" % (self.chipIdentifier.get_meas_count(self.lastRead))) 508 | 509 | self.update() 510 | 511 | if 'randomnessWindow' in self.__dict__: 512 | self.updateRandomnessWindow() 513 | 514 | 515 | def writeReport(self): 516 | reportFile = tkFileDialog.asksaveasfile(mode='w', 517 | defaultextension=".txt", 518 | filetypes=[("ASCII Text", ".txt")], 519 | title="Save Report As...") 520 | 521 | def fmtHeadingString(title, decorator="-"): 522 | return "\n" + title + "\n" + decorator*len(title) + "\n" 523 | 524 | def fmtNameAndSig(name, sig): 525 | return "Chip Name: " + name + "\n\nResponse: " + sig.hex 526 | 527 | def fmtUnstableBitMap(unstableBits): 528 | return "\nUnstable Bit Map: " + unstableBits.hex 529 | 530 | print >> reportFile, fmtHeadingString("PUF Analysis Report File", "=") 531 | 532 | 533 | if ('bitSource' in self.__dict__ and type(self.bitSource) == type(QuartusCon())): 534 | print >> reportFile, "Board Temperature: %0.2f deg. C, %0.2f deg. F" % \ 535 | (self.bitSource.get_temp(), self.bitSource.get_temp("F")) 536 | elif ('bitSource' in self.__dict__ and type(self.bitSource) == type(Simulator())): 537 | print >> reportFile, 'Simulator Setup: %s' % self.bitSource.getSetupStr() 538 | 539 | print >> reportFile, fmtHeadingString("Current Measurement") 540 | # Could draw an ASCII representation here 541 | print >> reportFile, fmtNameAndSig(self.lastRead, self.bits) 542 | print >> reportFile, fmtUnstableBitMap(self.chipIdentifier.unstableBits[self.lastRead]) 543 | 544 | print >> reportFile, fmtHeadingString("Scoreboard") 545 | scores = sorted(self.chipIdentifier.MatchMap(self.bits).items(), key=lambda item: item[1])[0:self.numMatchScores] 546 | for i in range(len(scores)): 547 | print >> reportFile, scores[i][0], "\t", '%0.2f %%' % (100-scores[i][1]*100) 548 | 549 | print >> reportFile, fmtHeadingString('PUF Metrics') 550 | if(self.measurementCounter>0): 551 | print >> reportFile, "Bit Flips: " + fmtFractionPercent(self.bitFlips, self.nb) 552 | 553 | if(self.chipIdentifier.unstable_bits_valid(self.lastRead)): 554 | print >> reportFile, "Unstable Bits: " + fmtFractionPercent(self.chipIdentifier.get_num_unstable_bits(self.lastRead), self.nb) 555 | print >> reportFile, "Average Noise Distance: " + fmtFractionPercent(self.chipIdentifier.get_noise_dist_avg(self.lastRead), self.nb) 556 | 557 | if (self.lastRead in self.chipIdentifier.interChipDistMap): 558 | print >> reportFile, "Average Inter-Chip Distance: " + fmtFractionPercent(self.chipIdentifier.get_inter_dist_avg(self.lastRead), self.nb) 559 | 560 | if self.chipIdentifier.get_meas_count(self.lastRead) > 2 and len(self.chipIdentifier) > 2: 561 | print >> reportFile, "Probability of Aliasing: " + ( "%.3e" % (self.chipIdentifier.prob_alias()[1]) ) 562 | 563 | print >> reportFile, "Measurement Count: " + ("Meas. #: %d" % (self.chipIdentifier.get_meas_count(self.lastRead))) 564 | 565 | print >> reportFile, fmtHeadingString("Randomness Checks") 566 | for i, (name, fun) in enumerate(self.randomnessFunMap.items()): 567 | fun_metric, fun_pass = fun(self.bits) 568 | print >> reportFile, "%20s %.10e" % (name, fun_metric), fun_pass 569 | 570 | print >> reportFile, fmtHeadingString("Other Signatures") 571 | for name, signature in self.chipIdentifier.signatureMap.items(): 572 | if (name != self.lastRead): 573 | print >> reportFile, fmtNameAndSig(name, signature)+"\n" 574 | 575 | def updateChipPicker(self): 576 | """This updates the optionmenu for picking a virtual chip from the sample of virtual chips. Applies only to the simulator. """ 577 | if (self.sourceSelected.get() == 'Simulator') and ('bitSource' in self.__dict__) and (type(self.bitSource) == Simulator): 578 | self.virtChipSelect = OptionMenu(self.buttonFrame, self.virtChipNumVar, *self.bitSource.chipNames) 579 | self.virtChipNumVar.set(self.bitSource.chipNames[0]) 580 | self.virtChipNumVar.trace('w', self.onVirtChipSelect) 581 | self.virtChipSelect.grid(row=0, column=2, padx=4, pady=4) 582 | elif ('virtChipSelect' in self.__dict__): 583 | self.virtChipSelect.grid_forget() 584 | 585 | def onVirtChipSelect(self, *args): 586 | """Handler for when new virtual chip has been selected""" 587 | self.reset() 588 | 589 | def open(self, event=None): 590 | """Open one of the available interfaces""" 591 | 592 | # Clean up if a connection is already open 593 | if ('bitSource' in self.__dict__): 594 | self.bitSource.close() 595 | del self.bitSource 596 | error = False 597 | 598 | # Reset error correction 599 | if ('corrector' in self.__dict__): 600 | del self.corrector 601 | 602 | sigFileName = os.path.join(self.outputPath, self.sourceSelected.get(), 'signatures.xml') 603 | 604 | if (self.sourceSelected.get() == 'Simulator'): 605 | self.bitSource = Simulator() 606 | self.bitSource.setup() 607 | if (not os.path.isfile(sigFileName)): 608 | print "Generating signature DB for simulator virtual chips...", 609 | self.bitSource.makeSigFile(sigFileName) 610 | print "OK" 611 | elif (self.sourceSelected.get() == 'File'): 612 | filename = tkFileDialog.askopenfilename( 613 | defaultextension=".dat", 614 | filetypes=[("Binary Data", ".dat")], 615 | title="Choose PUF Data File") 616 | if filename: 617 | self.bitSource = SigFile(filename, self.nb) 618 | sigFileName = self.loadSigFile() 619 | if not sigFileName: 620 | sigFileName = os.path.join(os.path.split(filename)[0], 'signatures.xml') 621 | else: 622 | error = True 623 | elif (self.sourceSelected.get() in self.quartusSources.keys()): 624 | self.bitSource = QuartusCon( 625 | tclFile=self.quartusSources[self.sourceSelected.get()]['tclFile'], 626 | cdf_filename=self.quartusSources[self.sourceSelected.get()]['cdf_filename']) 627 | self.bitSource.program() 628 | else: 629 | print 'Invalid source' 630 | 631 | if (not error): 632 | self.lastRead = "" 633 | self.chipIdentifier = ChipIdentify(sigFileName) 634 | self.reset() 635 | self.nextButton.config(state=NORMAL) 636 | self.closeButton.config(state=NORMAL) 637 | self.menuBarSource.entryconfig('Next', state=NORMAL) 638 | self.menuBarSource.entryconfig('Disconnect', state=NORMAL) 639 | self.menuBarChipDB.entryconfig('Clear', state=NORMAL) 640 | if (self.sourceSelected.get() == 'Simulator'): 641 | self.menuBarSource.entryconfig('Simulator', state=NORMAL) 642 | self.updateMenuBarSimulate() 643 | self.menuBarAnalyze.entryconfig('Save Report', state=NORMAL) 644 | self.updateStatus() 645 | self.updateChipPicker() 646 | 647 | def close(self, event=None): 648 | if ('bitSource' in self.__dict__): 649 | self.bitSource.close() 650 | del self.bitSource 651 | self.nextButton.config(state=DISABLED) 652 | self.closeButton.config(state=DISABLED) 653 | self.menuBarSource.entryconfig('Next', state=DISABLED) 654 | self.menuBarSource.entryconfig('Disconnect', state=DISABLED) 655 | self.menuBarAnalyze.entryconfig('Randomness Checks', state=DISABLED) 656 | self.menuBarAnalyze.entryconfig('Draw Histograms', state=DISABLED) 657 | self.menuBarChipDB.entryconfig('Clear', state=DISABLED) 658 | self.menuBarSource.entryconfig('Simulator', state=DISABLED) 659 | self.menuBarAnalyze.entryconfig('Save Report', state=DISABLED) 660 | self.updateChipPicker() 661 | 662 | def getChipDatPath(self, chip_name): 663 | return os.path.join(self.outputPath, self.sourceSelected.get(), str(chip_name) + '.dat') 664 | 665 | def next(self, event=None): 666 | if 'bitSource' not in self.__dict__: 667 | return 668 | 669 | if self.sourceSelected.get() == 'Simulator': 670 | new_bits = self.bitSource.next(self.virtChipNumVar.get()) 671 | else: 672 | new_bits = self.bitSource.next() 673 | 674 | # Determine chip's name 675 | if len(self.chipIdentifier)>0: 676 | chip_name, match_dist = self.chipIdentifier.Identify(new_bits) 677 | print "Best match for signature: %s with %6f Hamming distance" % (chip_name, match_dist) 678 | if len(self.chipIdentifier)==0 or match_dist > self.noiseThreshold: 679 | # Don't know this chip 680 | chip_name = tkSimpleDialog.askstring('Enter Chip Name', 'The noise threshold (%02d %%) has been exceeded or this is a new chip.\nPlease enter its name:' % (100*self.noiseThreshold), initialvalue=chip_name if len(self.chipIdentifier)>0 else '') 681 | self.chipIdentifier.add(chip_name, new_bits) 682 | self.chipIdentifier.save() # don't really need to do this until we close 683 | self.chipIdentifier.process_sig(chip_name, new_bits) # compute some greedy statistics 684 | 685 | # Don't write the bits in case of file read-back 686 | if self.sourceSelected.get() != 'File': 687 | if chip_name != self.lastRead: 688 | if 'sigFileWriter' in self.__dict__: 689 | self.sigFileWriter.close() 690 | filePath = self.getChipDatPath(chip_name) 691 | print "Saving PUF data to '%s'" % filePath 692 | self.sigFileWriter = SigFile(filePath) 693 | self.sigFileWriter.append(new_bits) 694 | 695 | # Error correction filter 696 | if (self.correctVar.get() == 1): 697 | if ('corrector' not in self.__dict__ or chip_name != self.lastRead): 698 | self.corrector = bch_code.bch_code() 699 | if (self.chipIdentifier.get_meas_count(self.lastRead)): 700 | self.corrector.setup(self.chipIdentifier.get_sig(self.lastRead)) 701 | else: 702 | self.corrector.setup(new_bits) 703 | print "ECC Enrollment: ", 704 | print "Syndrome:\n" + self.corrector.syndrome 705 | 706 | if (self.chipIdentifier.get_meas_count(self.lastRead) > 1): 707 | print "ECC Recovery: ", 708 | numErrors = hd(new_bits, self.chipIdentifier.get_sig(self.lastRead)) 709 | print "Errors from enrollment: %d" % numErrors 710 | if numErrors > self.corrector.t: 711 | print "ERROR: Error Correction Code strength %d not enough to correct %d errors" % (self.corrector.t, numErrors) 712 | else: 713 | try: 714 | corrected = self.corrector.decode(new_bits) 715 | print "Errors corrected: %d" % hd(new_bits, corrected) 716 | print "Errors after correction: %d" % hd(self.bits, corrected) 717 | new_bits = corrected 718 | except ValueError as e: 719 | print "Call to ECC process failed!" 720 | 721 | self.lastRead = chip_name 722 | 723 | # Report on unstable bits 724 | if self.chipIdentifier.unstable_bits_valid(self.lastRead): 725 | print "Unstable bits: %d / %d = %.3f %%" % (self.chipIdentifier.get_num_unstable_bits(self.lastRead), self.nb, (float(self.chipIdentifier.get_num_unstable_bits(self.lastRead))/self.nb)*100) 726 | print "Unstable bit map:" 727 | print repr(self.chipIdentifier.unstableBits[self.lastRead]) 728 | 729 | print "Measurement number: ", self.chipIdentifier.get_meas_count(chip_name) 730 | 731 | if (self.measurementCounter > 0): 732 | self.bitFlips = hd(self.bits, new_bits) 733 | elif (self.chipIdentifier.get_meas_count(chip_name) > 0): 734 | self.measurementCounter = self.chipIdentifier.get_meas_count(chip_name) 735 | self.bitFlips = hd(self.chipIdentifier.signatureMap[chip_name], new_bits) 736 | 737 | self.bits = new_bits 738 | self.updateBitAvgs() 739 | 740 | self.menuBarAnalyze.entryconfig('Randomness Checks', state=NORMAL) 741 | self.menuBarAnalyze.entryconfig('Draw Histograms', state=NORMAL) 742 | self.updateWidgets() 743 | self.measurementCounter += 1 744 | 745 | def setScale(self, event=None): 746 | self.zoomFactor = max([int(tkSimpleDialog.askstring('Enter Scale', 'Current scale: %d, current square dimension: %d\nEnter new scale (scale >= 1):' % (self.zoomFactor, self.squareSize))), 1]) 747 | self.sigCanvas.config(width=self.squareSize*self.zoomFactor, 748 | height=self.squareSize*self.zoomFactor) 749 | self.make_pi() 750 | if self.measurementCounter > 0: 751 | self.updateWidgets() 752 | 753 | def setFontSize(self, event=None): 754 | newFontSize = max([4, int(tkSimpleDialog.askstring('Enter Font Size', 'Current font size: %d\nEnter new font size (minimum 4):' % self.font['size']))]) 755 | self.font.configure(size=newFontSize) 756 | self.bigfont.configure(size=newFontSize+2) 757 | 758 | 759 | def loadSigFile(self, event=None): 760 | sigFileName = tkFileDialog.askopenfilename( 761 | defaultextension=".xml", 762 | filetypes=[("Signature XML File", ".xml")], 763 | title="Choose Signature DB File") 764 | if sigFileName != '': 765 | self.chipIdentifier = ChipIdentify(sigFileName) 766 | 767 | return sigFileName 768 | 769 | def clearSigFile(self): 770 | if tkMessageBox.askyesno("Confirm", "Are you sure you want to clear the Signature DB\nat '%s'?" % self.chipIdentifier.fileName): 771 | self.chipIdentifier.clear() 772 | self.updateWidgets() 773 | 774 | # This has enough functions and variables to become its own object 775 | def runRandomnessCheck(self): 776 | if 'randomnessWindow' not in self.__dict__: 777 | 778 | self.randomnessWindow = Toplevel() 779 | self.randomnessWindow.title("Randomness Checks") 780 | self.randomnessWindow.protocol("WM_DELETE_WINDOW", self.closeRandomnessWindow) 781 | 782 | self.randomnessLabels = [] 783 | self.randomnessFields = [] 784 | self.randomnessFieldVars = [] 785 | self.randomnessPass = [] 786 | self.randomnessPassVars = [] 787 | for i, (name, fun) in enumerate(self.randomnessFunMap.items()): 788 | self.randomnessLabels.append (Label(self.randomnessWindow, text=name, font=self.font)) 789 | self.randomnessLabels[i].grid(row=i, column=0, sticky='W') 790 | self.randomnessFieldVars.append(StringVar()) 791 | self.randomnessFields.append (Label(self.randomnessWindow, textvariable=self.randomnessFieldVars[i], font=self.font)) 792 | self.randomnessFields[i].grid(row=i, column=1) 793 | self.randomnessPassVars.append(StringVar()) 794 | self.randomnessPass.append( Label(self.randomnessWindow, textvariable=self.randomnessPassVars[i], font=self.font)) 795 | self.randomnessPass[i].grid(row=i, column=2) 796 | 797 | self.updateRandomnessWindow() 798 | 799 | def updateRandomnessWindow(self): 800 | for i, (name, fun) in enumerate(self.randomnessFunMap.items()): 801 | fun_metric, fun_pass = fun(self.bits) 802 | print "%20s %.5e" % (name, fun_metric), fun_pass 803 | self.randomnessFieldVars[i].set("%f" % fun_metric) 804 | self.randomnessPassVars[i].set("Pass" if fun_pass else "Fail") 805 | self.randomnessWindow.update() 806 | 807 | def closeRandomnessWindow(self): 808 | self.randomnessWindow.destroy() 809 | del self.randomnessWindow 810 | 811 | def runDistHist(self): 812 | import numpy 813 | import matplotlib.pyplot as plt 814 | 815 | frac_bits = self.distHistFractions.get() != 0 816 | plt.ion() # switch to interactive mode, else plot functions block GUI operation 817 | 818 | noise_dists = numpy.array(self.chipIdentifier.get_all_noise_dists(), numpy.double) 819 | inter_chip_dists = numpy.array(self.chipIdentifier.get_all_inter_chip_dists(), numpy.double) 820 | 821 | noise_threshold, prob_alias = self.chipIdentifier.prob_alias() 822 | title = "Noise and Inter-Chip Hamming Distances\nProbability of Aliasing: %1.3e" % prob_alias 823 | noise_label = 'Noise $\\mu=$' + \ 824 | ('%0.3f' % float(sum(noise_dists)/len(noise_dists)/self.nb) if frac_bits else 825 | ('%d' % (sum(noise_dists)/len(noise_dists)) ) ) + \ 826 | ', $N=$%d' % len(noise_dists) 827 | inter_chip_label = 'Inter-Chip $\\mu=$' + \ 828 | ('%0.3f' % float(sum(inter_chip_dists)/len(inter_chip_dists)/self.nb) if frac_bits else 829 | ('%d' % (sum(inter_chip_dists)/len(inter_chip_dists)) ) ) + \ 830 | ', $N=$%d' % len(inter_chip_dists) 831 | noise_threshold_label = "Noise Threshold = " + \ 832 | (("%1.3f" % (float(noise_threshold)/self.nb)) if frac_bits else 833 | ('%d' % math.ceil(noise_threshold))) 834 | xlabel = "Relative Hamming Distance (Response Length Fraction)" if frac_bits else "Hamming Distance" 835 | 836 | plt.clf() 837 | 838 | if self.distHistSelected.get() == 'Simple': 839 | plt.xlim(0, 1 if frac_bits else self.nb) 840 | plt.hist(noise_dists/(self.nb if frac_bits else 1), normed=True, cumulative=False, color='r', label=noise_label) 841 | plt.hist(inter_chip_dists/(self.nb if frac_bits else 1), normed=True, cumulative=False, color='b', label=inter_chip_label) 842 | plt.axvline(noise_threshold/(self.nb if frac_bits else 1), color='g', label=noise_threshold_label) 843 | plt.title(title) 844 | plt.xlabel(xlabel) 845 | plt.ylabel("Probability") 846 | plt.legend() 847 | elif self.distHistSelected.get() == 'Split': 848 | plt.subplot(211) 849 | plt.title(title) 850 | plt.hist(noise_dists/(self.nb if frac_bits else 1), normed=True, cumulative=False, color='r', label=noise_label) 851 | plt.axvline(noise_threshold/(self.nb if frac_bits else 1), color='g', label=noise_threshold_label) 852 | plt.ylabel("Probability") 853 | plt.legend() 854 | 855 | plt.subplot(212) 856 | plt.hist(inter_chip_dists/(self.nb if frac_bits else 1), normed=True, cumulative=False, color='b', label=inter_chip_label) 857 | plt.xlabel(xlabel) 858 | plt.ylabel("Probability") 859 | plt.legend() 860 | elif self.distHistSelected.get() == 'Cumulative': 861 | nd_hist, nd_bin_edges = numpy.histogram(noise_dists, density=True) 862 | nd_hist_cum = nd_hist.cumsum().astype(float) / sum(nd_hist) 863 | plt.plot(numpy.append(nd_bin_edges, self.nb)/(self.nb if frac_bits else 1), numpy.append(nd_hist_cum, [1, 1]), drawstyle='steps', color='r', label=noise_label) 864 | 865 | icd_hist, icd_bin_edges = numpy.histogram(inter_chip_dists, density=True) 866 | icd_hist_cum = icd_hist.cumsum().astype(float) / sum(icd_hist) 867 | plt.plot(numpy.append(icd_bin_edges, self.nb)/(self.nb if frac_bits else 1), numpy.append(icd_hist_cum, [1, 1]), drawstyle='steps', color='b', label=inter_chip_label) 868 | 869 | plt.axvline(noise_threshold/(self.nb if frac_bits else 1), color='g') 870 | 871 | plt.title(title) 872 | plt.xlabel(xlabel) 873 | plt.ylabel("Probability") 874 | plt.legend(loc='lower right') # show the legend 875 | plt.axis([0, 1 if frac_bits else self.nb, 0, 1]) 876 | 877 | 878 | print __copyright__ 879 | root = Tk() 880 | app = Application(master=root) 881 | app.mainloop() 882 | --------------------------------------------------------------------------------