{"id":28124629,"url":"https://github.com/stnolting/fpga_puf","last_synced_at":"2025-05-14T09:19:09.829Z","repository":{"id":50292732,"uuid":"428800193","full_name":"stnolting/fpga_puf","owner":"stnolting","description":":key: Technology-agnostic Physical Unclonable Function (PUF) hardware module for any FPGA.","archived":false,"fork":false,"pushed_at":"2022-11-05T18:07:16.000Z","size":50,"stargazers_count":71,"open_issues_count":0,"forks_count":8,"subscribers_count":14,"default_branch":"main","last_synced_at":"2023-04-12T11:24:47.903Z","etag":null,"topics":["authentication","fingerprint","fpga","key-generation","neorv32","puf","security","vhdl"],"latest_commit_sha":null,"homepage":"","language":"VHDL","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-3-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/stnolting.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2021-11-16T20:15:24.000Z","updated_at":"2023-04-12T10:54:40.000Z","dependencies_parsed_at":"2022-08-25T07:00:25.653Z","dependency_job_id":null,"html_url":"https://github.com/stnolting/fpga_puf","commit_stats":null,"previous_names":[],"tags_count":null,"template":null,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stnolting%2Ffpga_puf","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stnolting%2Ffpga_puf/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stnolting%2Ffpga_puf/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stnolting%2Ffpga_puf/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/stnolting","download_url":"https://codeload.github.com/stnolting/fpga_puf/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254110433,"owners_count":22016392,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["authentication","fingerprint","fpga","key-generation","neorv32","puf","security","vhdl"],"created_at":"2025-05-14T09:18:41.939Z","updated_at":"2025-05-14T09:19:09.817Z","avatar_url":"https://github.com/stnolting.png","language":"VHDL","funding_links":[],"categories":[],"sub_categories":[],"readme":"# :closed_lock_with_key: A Physical Unclonable Function for _any_ FPGA\n\n[![License](https://img.shields.io/github/license/stnolting/fpga_puf)](https://github.com/stnolting/fpga_puf/blob/main/LICENSE)\n[![DOI](https://zenodo.org/badge/428800193.svg)](https://zenodo.org/badge/latestdoi/428800193)\n\n* [Introduction](#Introduction)\n* [Theory of Operation](#Theory-of-Operation)\n   * [Post-Processing](#Post-Processing)\n* [Top Entity](#Top-Entity)\n* [Evaluation](#Evaluation)\n   * [Setup](#Setup)\n   * [ID Results](#ID-Results)\n   * [Reliability](#Reliability)\n   * [Hardware Utilization](#Hardware-Utilization)\n* [Summary](#Summary)\n* [TODO](#TODO)\n\n\n## Introduction\n\nA physical unclonable function (PUF) provides a digital _fingerprint_ that can be used as **unique identifier** for\nsecurity-related applications like authentication. The `fpga_puf` hardware module provides an unclonable 96-bit unique identifier (ID)\nthat is defined by the target chip's semiconductor characteristics. It is implemented in a technology-independent\nway that does not use any device-specific macros, primitives or attributes so it can be implemented on _any_ FPGA\n(verified using Intel Quartus Prime, Lattice Radiant and Xilinx Vivado).\n\n:key: The PUF ID is _unique_ for the combination of bitstream and _one_ specific FPGA. The same bitstream\nwill lead to a different PUF ID on a different FPGA of same type. If the bitstream of a specific FPGA is changed\n(by changing design logic) the PUF ID of that specific FPGA will also change.\n\n**Key features**\n\n* [x] technology-, vendor- and platform-agnostic implementation\n* [x] easy to use\n* [x] tiny hardware footprint (less than 200 LUTs)\n* [ ] secure and reliable (:construction: work-in-progress)\n\n:loudspeaker: This is an ongoing research / proof-of-concept project. Feedback from the community is highly appreciated\n(see notes in section [\"TODO\"](#TODO))!\n\n\n## Theory of Operation\n\nEach bit of the 96-bit PUF ID is generated by an individual **PUF cell**. Each cell consists of an asynchronous element that is\nbased on a simple ring oscillator. The oscillator is constructed from a single inverter that negates it's own output signal.\nThis feedback loop is interrupted by a latch. If the latch is open (=transparent) the oscillator starts oscillating.\nIf the latch is closed, it stores the last state of the oscillator (high or low). The latch also provides a reset\nto bring each cell into a defined state.\n\nThe frequency of each oscillator is defined by the mapping of the logic cell and the according routing. Both factors\nare constant for a specific bitstream. Furthermore, the frequency is also randomly (but constantly for a given setup)\n\"tuned\" by the chip's semiconductor characteristics.\nThese are caused by tiny productions fluctuation (for example the capacitance / delay of a routing wire is affected\nby variation in oxide thickness). The frequency of each oscillator is considered to be fixed. Hence, sampling several periods\nwithin a fixed time window will always end in the same state (oscillator output is high or low). Temperature drift\ndisturbs all oscillators in the (nearly) same way and has to be computationally eliminated/compensated by the\n[post-processing](#Post-Processing).\n\nSince `fpga_puf` does not use any device-specific attributes or primitives, there is a risk that the synthesis toolchain will\nremove the asynchronous PUF cells or collapse them into a single PUF cell (\"optimize away\"). Therefore, the design\nuses a shift register to control reset and the latch open/close phase of each cell individually and distributed over time. This concept is\nbased on the [neoTRNG True Random Number Generator](https://github.com/stnolting/neoTRNG) and allows a **platform-independent implementation**.\n\nWhenever a new sampling of the PUF ID is started, a 96+1 bit wide shift register. A single '1' is applied to the\nleast significant bit that travels throughout the whole shift register chain during operation. The PUF cell's control signals (reset and\nlatch control) are connected to this shift register. Each shift register bit `i` controls the reset of PUF cell\n`i` and the latch control of PUF cell `i+1`. Thus, a cell's reset is active for one clock cycle. In the next clock\ncycle the cell's latch is opened for exactly one clock cycle allowing the oscillator to run. In the next clock cycle\nthe latch is closed again and has captured the oscillator's last state. By this scheme only one oscillator is active\nat a time reducing potential distortion by oscillator cross-talk (i.e. the oscillation of one osc. affect the\noscillations of another osc.).\n\nWhen the single one-bit reaches the most significant bit (bit 96) of the shift register the sampling process is\ncompleted and the latch states are sampled into a register that provides the obtained _raw_ PUF ID.\n\nThe following figure is taken from the Intel Quartus Prime technology viewer showing a single PUF cell (cell #44).\nThe oscillator as well as the latch are mapped to a single LUT3 element in the FPGA. The output of this asynchronous\nelement is sampled by a simple register. Hence, a single PUF cell only requires one LUT and one FF.\n\n(`DATAA` is the latch reset (high-active), `DATAC` is the latch control (open when set), `DATAD` is the\nfeedback of the inverter)\n\n![puf_cell](https://raw.githubusercontent.com/stnolting/fpga_puf/main/img/intel_technology_cell.png)\n\n### Post-Processing\n\nSome bits of the _raw_ PUF ID might be quite noisy, which complicates defining a _stable_ ID that does not\nchange over time and over a broad range of operation conditions (e.g. temperature). A lot(!) of different\napproaches can be found in literatures. One promising approach is the use of error-correction codes to \"stabilizes\"\nan initially determined ID.\n\nHowever, these concepts are out of (my) scope so I used a simple \"averaging\" concept for this example. My approach samples\nthe _raw_ PUF ID several times (for example 4096 times) and checks how often each bit of the ID is set across\nall sampled IDs. If an ID bit is set in more then half of the samples it is considered to be a static `1`, otherwise it is considered to\nbe a static `0`. Since a few bits of the raw ID might be quite noisy, a hysteresis is used to eliminate those bits from the final PUF:\nif a bit is set or cleared more often than a certain _threshold_ it is considered \"stable\" in the final ID. These lower and\nupper hysteresis threshold define an _uncertain_ band between them. For the final ID all bits tha fall\ninto this uncertainty band are masked to be always zero.\n\nMy post-processing concept can be found in [`sw/main.c`](https://github.com/stnolting/fpga_puf/blob/main/sw/main.c). The\nsource file provides more-detailed comments.\n\n\n## Top Entity\n\nThe top entity of the PUF IP module is `fpga_puf` ([`rtl/fpga_puf.vhd`](https://github.com/stnolting/fpga_puf/blob/main/rtl/fpga_puf.vhd)).\nIt can be directly instantiated without the need for any special libraries.\n\n```vhdl\nentity fpga_puf is\n  port (\n    clk_i  : in  std_ulogic; -- global clock line\n    rstn_i : in  std_ulogic; -- SYNC reset, low-active\n    trig_i : in  std_ulogic; -- set high for one clock to trigger ID sampling\n    busy_o : out std_ulogic; -- busy when set (sampling ID)\n    id_o   : out std_ulogic_vector(95 downto 0) -- PUF ID (valid after sampling is done)\n  );\nend fpga_puf;\n```\n\n:warning: Note that the PUF cannot be simulated due to it's combinatorial loops.\n\n## Evaluation\n\nTo evaluate this concept and the quality/reliability of the PUF IDs I am using the [NEORV32](https://github.com/stnolting/neorv32)\nas processor platform. The `fpga_puf` IP module is added to the processor's \"Custom Functions Subsystem (CFS)\",\nwhich is a _blank_ template for implementing custom application-specific co-processors.\n\nThe setup is synthesized for different FPGAs using different toolchains (tested with Intel Quartus Prime, Lattice Radiant and\nXilinx Vivado). A specific bitstream generated and programmed into _several_ FPGAs of the _same_ type to check for chip-specific\nID variations. On one chip the ID is generated several times to check if it is \"stable over time\" and thus, reliable.\n\n### Setup\n\nThe application-specific PUF-wrapping CFS can be found in\n[`rtl/neorv32_cfs.vhd`](https://github.com/stnolting/fpga_puf/blob/main/rtl/neorv32_cfs.vhd). Make sure to use this\nCFS file instead of the default NEORV32 CFS source file when reproducing this setup.\n\nThe CFS uses four memory-mapped registers to interface the PUF ID module:\n\n| CSF register address (_C_ access macro) | Access | Function          |\n|:----------------------------------------|:------:|:------------------|\n| `NEORV32_CFS.REG[0]`                    |  `r/w` | Control register  |\n| `NEORV32_CFS.REG[1]`                    |  `r/-` | PUF ID bits 31:0  |\n| `NEORV32_CFS.REG[2]`                    |  `r/-` | PUF ID bits 63:32 |\n| `NEORV32_CFS.REG[3]`                    |  `r/-` | PUF ID bits 95:64 |\n\nBit _0_ of the control register (`PUF_CTRL_EN`) control the synchronous reset signal of the `fpga_puf` module\n(`rstn_i`). Setting this bit will activate the module, clearing it will put the module into reset state.\n\nBit _1_ of the control register (`PUF_CTRL_SAMPLE`) is used to start the sampling of the ID. Writing one to it\nwill set the trigger signal (`trig_i`) high for one cycle. Reading this control register bit will return\nthe busy state of the `fpga_puf` module (`busy_o`).\n\n:floppy_disk: The low-level hardware-accessing functions are implemented in\n[`sw/fpga_puf_neorv32_cfs.c`](https://github.com/stnolting/fpga_puf/blob/main/sw/fpga_puf_neorv32_cfs.c). The header file\n[`sw/fpga_puf_neorv32_cfs.h`](https://github.com/stnolting/fpga_puf/blob/main/sw/fpga_puf_neorv32_cfs.h)\nprovides the function prototypes, a PUF ID data type and also the NEORV32 CFS register mappings and bit definitions.\n\nThe PUF test program ([`sw/main.c`](https://github.com/stnolting/fpga_puf/blob/main/sw/main.c)) is used to sample\nthe chip's PUF ID and transmit it via UART to a terminal program:\n\n```\nPhysical Unclonable Functions \u003cfpga_puf\u003e Test\nPUF implemented as NEORV32 Custom Functions Subsystem (CFS)\n\nPress any key to start PUF test (8 runs with 4096 samples each).\nStarting test...\nRun 0 ID: 0x37c0480063021011988c0095\nRun 1 ID: 0x37c0480063021011988c0095\nRun 2 ID: 0x37c0480063021011988c0095\nRun 3 ID: 0x37c0480063021011988c0095\nRun 4 ID: 0x37c0480063021011988c0095\nRun 5 ID: 0x37c0480063021011988c0095\nRun 6 ID: 0x37c0480063021011988c0095\nRun 7 ID: 0x37c0480063021011988c0095\nTest completed.\n```\n\n:floppy_disk: A pre-compiled NEORV32 executable of this test program is also available in this\nrepository: [`sw/neorv32_exe.bin`](https://github.com/stnolting/fpga_puf/blob/main/sw/neorv32_exe.bin)\n(compiled for a minimal `rv32i` NEORV32 CPU)\n\n### ID Results\n\nSo far I have tested the PUF module on three Lattice iCE40 UltraPlus FPGAs and four Intel Cyclone IV FPGAs\n(shout-out to [@emb4fun](https://github.com/emb4fun) - thank you for your help!) and just one Xilinx Artix-7\nFPGA. The ID was generated 8 times on each chip to \"check\" if it is reproducible (drawback: just in a very\nshort time window...). The specific FPGA types are shown in section [Hardware Utilization](#Hardware-Utilization).\n\n| FPGA                            | PUF ID (\"Fingerprint Key\")   |\n|:--------------------------------|:-----------------------------|\n| Lattice iCE40 UltraPlus - **1** | `0x37c0480063021011988c0095` |\n| Lattice iCE40 UltraPlus - **2** | `0x592063e50118040c2000112b` |\n| Lattice iCE40 UltraPlus - **3** | `0x941c82505112323600b0c221` |\n| Intel Cyclone IV - **1**        | `0x9fbe33dc9021be3156cae31b` |\n| Intel Cyclone IV - **2**        | `0xa726db495a2a8346b30f2000` |\n| Intel Cyclone IV - **3**        | `0x413fe25557311be7f9f5edfb` |\n| Intel Cyclone IV - **4**        | `0xca8ee8dc7945fa60f770e1fa` |\n| Xilinx Artix-7 - **1**          | `0x6faaf93af77cf77fef91fe79` |\n\nSo far, the IDs are _unique_ for each tested FPGA!\n\n### Reliability\n\n**:construction: work in progress :construction:**\n\nA long-time test is used to sample and check _raw_ IDs (not the pre-processed ones) for _stability over time_.\nNote that my setup is placed in pretty stable environment conditions.\n\nThe test generates an _initital_ ID `I` right at the beginning of execution. In an endless loop two consecutive\nIDs `A` and `B` are sampled (a single \"Run\"). To evaluate the the \"instability\" the Hamming distance (number of bits that are\nnot identical across two samples) is computed. This test computes the _relative_ Hamming distance of the two\nconsecutive samples `A` and `B` (= `H(A,B)`) and also the relative Hamming distance between the initial sample\nand sample `A` (= `H(I,A)`) from the current run. Furthermore, the maximum ob both distances is computed over\nall runs (`H_max(A,B)` and `H_max(I,A)`).\n\nTo identify _noisy bits_ an \"accumulated bit-change-mask\" `F` is computed. This mask is computed for every run\nby XOR-ing the obtained samples `A` and `B` with the initial ID `I`. The mask from a run is OR-ed with the mask from\nthe previous run to accumulate the noisy bits over time.\n\nCut-out of the test log:\n```\n...\nRun 43373: I=0x9fae9dd83029bc7156cbe37b, A=0xbfbe3bfc9423beb156cae31b, B=0xbfbe3bfcb021beb156cae31b - F=0x6038be24ac1e83c080214860 (33) - H(A,B)=3, H_max(A,B)=9 - H(I,A)=19, H_max(I,A)=23\nRun 43374: I=0x9fae9dd83029bc7156cbe37b, A=0xbfbe3bfc9435beb156eae31b, B=0xbfbe3bfcb431beb156eae31b - F=0x6038be24ac1e83c080214860 (33) - H(A,B)=2, H_max(A,B)=9 - H(I,A)=21, H_max(I,A)=23\nRun 43375: I=0x9fae9dd83029bc7156cbe37b, A=0xbfbe3bfc9425beb156caeb1b, B=0xbfbe3bdcb421beb156cae31b - F=0x6038be24ac1e83c080214860 (33) - H(A,B)=4, H_max(A,B)=9 - H(I,A)=20, H_max(I,A)=23\nRun 43376: I=0x9fae9dd83029bc7156cbe37b, A=0xbfbe3bfc9425beb156eaeb1b, B=0xbfbe3bfcb021beb156eae31b - F=0x6038be24ac1e83c080214860 (33) - H(A,B)=4, H_max(A,B)=9 - H(I,A)=21, H_max(I,A)=23\nRun 43377: I=0x9fae9dd83029bc7156cbe37b, A=0xbfbe3bfc9021beb156caeb1b, B=0xbfbe3bdc9425beb156cae31b - F=0x6038be24ac1e83c080214860 (33) - H(A,B)=4, H_max(A,B)=9 - H(I,A)=18, H_max(I,A)=23\n...\n```\n\nThis very first test was run for approx. half an hours making ~20 runs per second.\nIt shows a maximal Hamming distance of 23 bits while 33 bits (`F`) tend to be noisy (compared to the initial ID `I`\nsampled once right at the beginning of the test). The number of noisy bits increases slowly over time, probably\nbecause of increasing chip temperature. It tends to increase slower over time, so there might be a saturation at some point.\nThe temperature of the PUF cells is most important because that impacts the oscillator frequencies.\nThe PUF cells heat up due to the _continuous_ PUF operation.\n\nThe PUF from this test setup provides 96-33=**63** bits that seem to be stable over the observed time.\nThis also means that the usable key space is reduced (63-bit instead of 96-bit).\n\n### Hardware Utilization\n\nMapping results for the custom function subsystem (CFS), the `fpga_puf` module and a single PUF cell.\n\n| Lattice ice40 UltraPlus `iCE40UP5K-SG48I` @24MHz           | Logic Cells | Logic Registers |\n|:-----------------------------------------------------------|------------:|----------------:|\n| neorv32_cfs_inst_true.neorv32_cfs_inst                     |    171 (71) |        232 (35) |\n| -fpga_puf_inst                                             |     100 (4) |       197 (101) |\n| --fpga_puf_cell_inst[0].fpga_puf_cell_inst_i               |       1 (1) |           1 (1) |\n\n| Intel Cyclone IV `EP4CE22F17C6N` @100MHz                   | Logic Cells | Logic Registers |\n|:-----------------------------------------------------------|------------:|----------------:|\n| neorv32_cfs:\\neorv32_cfs_inst_true:neorv32_cfs_inst        |    241 (43) |        232 (35) |\n| -fpga_puf:fpga_puf_inst                                    |   198 (102) |       197 (101) |\n| --fpga_puf_cell:\\fpga_puf_cell_inst:0:fpga_puf_cell_inst_i |       1 (1) |           1 (1) |\n\n| Xilinx Artix-7 `XC7A35TICSG324-1L` @100MHz                   | Logic Cells | Logic Registers |\n|:-------------------------------------------------------------|------------:|----------------:|\n| neorv32_cfs_inst_true.neorv32_cfs_inst (neorv32_cfs)         |         133 |             328 |\n| -fpga_puf_inst (fpga_puf)                                    |         133 |             293 |\n| --fpga_puf_cell_inst[0].fpga_puf_cell_inst_i (fpga_puf_cell) |           1 |  2 (Latch + FF) |\n\n:information_source: Xilinx Vivado can detect the PUF cell's latch and infers a FF primitive, which also provides\na latch mode. This does not compromise the functionality of the PUF.\n\n## Summary\n\nThe PUF IDs are unique for each FPGA and can be successfully implemented on different FPGAs (Xilinx, Intel, Lattice).\n\nThe **raw PUF ID** is only partly reproducible, because some bits tend to be quite noisy (see\nresults above). A better post-processing algorithm using error-correction codes should be able to compensate for that.\nThis is **work in progress**.\n\nThe latest test showed that there is a certain number of bits that are quite noisy so they cannot be used to determine\nthe PUF IF. Obviously, this reduces the maximum key space (for example only 63-bit of the 96-bit ID are usable). To circumvent this,\nthe PUF can be made arbitrarily wide (for example providing a 256-bit raw ID). Of course this will also introduce additional\nunusable noisy bits, but we expect that the percentage of noisy bits within the PUF ID is a device-specific constant.\nEach additional PUF ID bit adds 2 FFs (one for the SREG and one for the PUFF cell) and 1 LUT. For each additional bit the\nID sampling time is increased by one clock cycle.\n\n:loudspeaker: If you have any kind of ideas or feedback (for example how to improve reliability) feel free to open a new\n[issue](https://github.com/stnolting/fpga_puf/issues). I am also happy to get more data, so if you\nhave ported the design on another FPGA you can open a [pull request](https://github.com/stnolting/fpga_puf/pulls)\nand add your results.\n\n## TODO\n\n* test more FPGAs\n* check stability in a controlled environment (e.g. temperature chamber)\n* evaluate more sophisticated post-processing algorithms\n* sample more data from more long-time test\n  * faulty bits over time (and temperature)\n  * hamming distance over time (and temperature)\n  * ...\n* to be continued...\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstnolting%2Ffpga_puf","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fstnolting%2Ffpga_puf","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstnolting%2Ffpga_puf/lists"}