├── .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 |
--------------------------------------------------------------------------------