├── tests ├── __init__.py ├── test_utils.py ├── test_attacks.py └── test_locks.py ├── docs ├── templates │ ├── credits.mako │ ├── head.mako │ ├── logo.mako │ ├── config.mako │ ├── text.mako │ ├── pdf.mako │ ├── css.mako │ └── html.mako ├── index.html ├── metrics.html ├── utils.html └── attacks.html ├── .flake8 ├── logiclocking ├── __init__.py ├── metrics.py ├── utils.py ├── attacks.py └── locks.py ├── .gitignore ├── Makefile ├── setup.py ├── LICENSE ├── .pre-commit-config.yaml ├── .github └── workflows │ └── python-package.yml └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/templates/credits.mako: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/templates/head.mako: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | select = C,E,F,W,B,B950 3 | extend-ignore = E203,W503,E501,E741 4 | exclude = 5 | .git, 6 | __pycache__, 7 | pepr.egg-info 8 | docstring-convention = google 9 | -------------------------------------------------------------------------------- /docs/templates/logo.mako: -------------------------------------------------------------------------------- 1 |
2 | 3 | GitHub 4 |
5 | -------------------------------------------------------------------------------- /logiclocking/__init__.py: -------------------------------------------------------------------------------- 1 | """Implementations of logic locking techniques and attacks.""" 2 | 3 | from logiclocking.utils import ( 4 | check_for_difference, 5 | locked_unroll, 6 | read_key, 7 | write_key, 8 | ) 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | html/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | pip-wheel-metadata/ 25 | share/python-wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # Genus 32 | *.log* 33 | *.cmd* 34 | fv/ 35 | .rs_* 36 | 37 | # Verilator 38 | obj_dir/ 39 | 40 | # Coverage 41 | htmlcov/ 42 | .coverage 43 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | doc : docs/index.html 3 | 4 | docs/index.html : logiclocking/* docs/templates/* 5 | pdoc --html logiclocking --force --template-dir docs/templates 6 | cp html/logiclocking/* docs 7 | 8 | test : 9 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 10 | python3 -m unittest 11 | 12 | test_% : 13 | python3 -m unittest logiclocking/tests/test_$*.py 14 | 15 | coverage : 16 | coverage run -m unittest 17 | coverage html 18 | 19 | dist : setup.py 20 | rm -rf dist/* build/* circuitgraph.egg-info 21 | python3 setup.py sdist bdist_wheel 22 | 23 | test_upload: dist 24 | python3 -m twine upload --repository testpypi dist/* 25 | 26 | upload : dist 27 | python3 -m twine upload dist/* 28 | 29 | install: 30 | pip3 install . 31 | 32 | install_editable : 33 | pip3 install -e . 34 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="logiclocking", 8 | version="0.0.1", 9 | author="Ruben Purdy, Joseph Sweeney", 10 | author_email="rpurdy@andrew.cmu.edu, joesweeney@cmu.edu", 11 | description="Tools for locking circuits and attacking locked circuits.", 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | url="https://github.com/circuitgraph/logiclocking", 15 | project_urls={ 16 | "Documentation": "https://circuitgraph.github.io/logiclocking/", 17 | "Source": "https://github.com/circuitgraph/logiclocking", 18 | }, 19 | include_package_data=True, 20 | packages=["logiclocking"], 21 | classifiers=[ 22 | "Programming Language :: Python :: 3", 23 | "License :: OSI Approved :: MIT License", 24 | "Operating System :: OS Independent", 25 | ], 26 | python_requires=">=3.6", 27 | install_requires=["circuitgraph>=0.2.0"], 28 | ) 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Ruben Purdy, Joseph Sweeney 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.4.0 4 | hooks: 5 | - id: check-ast 6 | - id: check-docstring-first 7 | - id: check-toml 8 | - id: check-yaml 9 | - id: end-of-file-fixer 10 | - id: mixed-line-ending 11 | - id: name-tests-test 12 | - id: sort-simple-yaml 13 | - id: trailing-whitespace 14 | - repo: https://github.com/pycqa/isort 15 | rev: 5.12.0 16 | hooks: 17 | - id: isort 18 | - repo: https://github.com/psf/black 19 | rev: 23.3.0 20 | hooks: 21 | - id: black 22 | - repo: https://github.com/PyCQA/docformatter 23 | rev: v1.6.0 24 | hooks: 25 | - id: docformatter 26 | additional_dependencies: [tomli] 27 | args: ["--in-place", "--config", "./pyproject.toml"] 28 | - repo: https://github.com/pycqa/flake8 29 | rev: 6.0.0 30 | hooks: 31 | - id: flake8 32 | additional_dependencies: [flake8-docstrings] 33 | - repo: https://github.com/pycqa/pydocstyle 34 | rev: 6.3.0 35 | hooks: 36 | - id: pydocstyle 37 | files: ^logiclocking/ 38 | args: ["--convention=numpy"] 39 | -------------------------------------------------------------------------------- /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Python package 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | python-version: ["3.8", "3.9", "3.10"] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v2 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | - name: Install dependencies 28 | run: | 29 | python -m pip install --upgrade pip 30 | python -m pip install flake8 31 | python -m pip install python-sat 32 | python -m pip install scikit-learn 33 | python -m pip install . 34 | - name: Lint with flake8 35 | run: | 36 | # stop the build if there are Python syntax errors or undefined names 37 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 38 | - name: Test with unittest 39 | run: | 40 | python -m unittest 41 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | import unittest 3 | 4 | import circuitgraph as cg 5 | 6 | from logiclocking import check_for_difference, locked_unroll, locks, read_key, write_key 7 | 8 | 9 | class TestUtils(unittest.TestCase): 10 | def test_check_for_difference(self): 11 | c0 = cg.from_lib("c17") 12 | o = c0.outputs().pop() 13 | 14 | c1 = c0.copy() 15 | c1.set_output(o, False) 16 | o_pre = c1.uid(f"{o}_pre") 17 | c1.relabel({o: o_pre}) 18 | k = c1.uid("key_0") 19 | c1.add(k, "input") 20 | c1.add(o, "xor", fanin=[o_pre, k], output=True) 21 | self.assertFalse(check_for_difference(c0, c1, {k: False})) 22 | self.assertTrue(check_for_difference(c0, c1, {k: True})) 23 | 24 | def test_locked_unroll(self): 25 | c = cg.from_lib("s27") 26 | cl, key = locks.trll(c, 8, shuffle_key=False) 27 | num_copies = 4 28 | clu, cu, io_map = locked_unroll(cl, key, num_copies, initial_values="0") 29 | self.assertEqual(len(cu.inputs()), (len(c.inputs()) - 1) * num_copies) 30 | self.assertEqual(len(cu.outputs()), len(c.outputs()) * num_copies) 31 | self.assertSetEqual(cu.inputs(), clu.inputs() - set(key)) 32 | self.assertSetEqual(cu.outputs(), clu.outputs()) 33 | self.assertFalse(check_for_difference(cu, clu, key)) 34 | 35 | wrong_key = {k: not v for k, v in key.items()} 36 | self.assertTrue(check_for_difference(cu, clu, wrong_key)) 37 | 38 | def test_read_write_key(self): 39 | key = {"k0": True, "k1": False} 40 | with tempfile.NamedTemporaryFile( 41 | mode="w", prefix="logiclocking_test_utils_test_read_write_key" 42 | ) as f: 43 | write_key(key, f.name) 44 | self.assertDictEqual(key, read_key(f.name)) 45 | -------------------------------------------------------------------------------- /tests/test_attacks.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import circuitgraph as cg 4 | 5 | from logiclocking import attacks, locks 6 | 7 | 8 | class TestAttacks(unittest.TestCase): 9 | def test_miter_attack_xor_lock(self): 10 | c = cg.from_lib("c17_gates") 11 | cl, key = locks.xor_lock(c, 8) 12 | result = attacks.miter_attack(cl, key, verbose=False, code_on_error=False) 13 | self.assertTrue(result["Equivalent"]) 14 | 15 | def test_miter_attack_sfll_hd(self): 16 | c = cg.from_lib("c17_gates") 17 | cl, key = locks.sfll_hd(c, 4, 3) 18 | result = attacks.miter_attack(cl, key, verbose=False, code_on_error=False) 19 | self.assertTrue(result["Equivalent"]) 20 | 21 | def test_cyclic_miter_attack_mux_lock(self): 22 | c = cg.from_lib("c17_gates") 23 | cl, key = locks.mux_lock(c, 8) 24 | while not cl.is_cyclic(): 25 | cl, key = locks.mux_lock(c, 8) 26 | with self.assertRaises(ValueError): 27 | result = attacks.miter_attack( 28 | cl, key, verbose=False, code_on_error=False, unroll_cyclic=False 29 | ) 30 | 31 | result = attacks.miter_attack(cl, key, verbose=False, code_on_error=False) 32 | self.assertTrue(result["Equivalent"]) 33 | 34 | def test_cyclic_miter_attack_full_lock(self): 35 | c = cg.from_lib("c17_gates") 36 | cl, key = locks.full_lock(c, 8, 2) 37 | while not cl.is_cyclic(): 38 | cl, key = locks.full_lock(c, 8, 2) 39 | result = attacks.miter_attack(cl, key, verbose=False, code_on_error=False) 40 | self.assertTrue(result["Equivalent"]) 41 | 42 | def test_decision_tree_attack(self): 43 | c = cg.from_lib("c499") 44 | cl, key = locks.xor_lock(c, 8) 45 | attacks.decision_tree_attack(cl, 100, key, verbose=False) 46 | attacks.decision_tree_attack(c, 100, verbose=False) 47 | -------------------------------------------------------------------------------- /logiclocking/metrics.py: -------------------------------------------------------------------------------- 1 | """Cacluate logic-locking metrics.""" 2 | from random import random 3 | from statistics import mean 4 | 5 | import circuitgraph as cg 6 | 7 | 8 | def corruptibility(cl, key): 9 | """Apprixmate corruptability of a locked circuit for a specific key.""" 10 | # set up miter 11 | ins = set(cl.startpoints() - key.keys()) 12 | m = cg.tx.miter(cl, startpoints=ins) 13 | set_key = {f"c0_{k}": v for k, v in key.items()} 14 | independent_set = {f"c1_{k}" for k in key} | ins 15 | 16 | # run approx 17 | errors = cg.sat.approx_model_count(m, {**set_key, "sat": True}, independent_set) 18 | 19 | return errors / 2 ** len(independent_set) 20 | 21 | 22 | def key_corruption(cl, key, attack_key): 23 | """Approximate corruption between two keys.""" 24 | # set up miter 25 | ins = set(cl.startpoints() - key.keys()) 26 | m = cg.tx.miter(cl, startpoints=ins) 27 | c0_key = {f"c0_{k}": v for k, v in key.items()} 28 | c1_key = {f"c1_{k}": v for k, v in attack_key.items()} 29 | 30 | # run approx 31 | errors = cg.sat.approx_model_count(m, {**c0_key, **c1_key, "sat": True}, ins) 32 | 33 | return errors / 2 ** len(ins) 34 | 35 | 36 | def min_corruption(cl, key, e=0.1, min_samples=10, tol=0.1): 37 | """Approximate the minimum corruption.""" 38 | # find total errors 39 | cor = corruptibility(cl, key) 40 | 41 | # get initial key corruptions 42 | key_corruptions = [] 43 | for _ in range(min_samples): 44 | sampled_key = {k: random() < 0.5 for k in key} 45 | key_corruptions.append(key_corruption(cl, key, sampled_key)) 46 | 47 | # sample until distribution matches 48 | while abs(mean(key_corruptions) - cor) > tol: 49 | sampled_key = {k: random() < 0.5 for k in key} 50 | key_corruptions.append(key_corruption(cl, key, sampled_key)) 51 | 52 | return len([kc for kc in key_corruptions if kc >= e]) / len(key_corruptions) 53 | 54 | 55 | def avg_avg_sensitivity(cl, key=None): 56 | """Get the average average sensitivity under a given key.""" 57 | # set key 58 | if key: 59 | cl_key = cl.copy() 60 | for k, v in key.items(): 61 | cl_key.set_type(k, v) 62 | else: 63 | cl_key = cl 64 | 65 | avg_sens = [] 66 | for o in cl.outputs(): 67 | # estimate sen 68 | avg_sens.append(cl.avg_sensitivity(o, e=3, d=0.8) / len(cl.startpoints(o))) 69 | 70 | return mean(avg_sens) 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CircuitGraph LogicLocking 2 | 3 | [![Python package](https://github.com/circuitgraph/logiclocking/actions/workflows/python-package.yml/badge.svg)](https://github.com/circuitgraph/logiclocking/actions/workflows/python-package.yml) 4 | [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) 5 | [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit) 6 | 7 | 8 | Implementations of various logic locks and attacks. 9 | 10 | ## Overview 11 | 12 | This library provides both locks and attacks on logic locks. Interfacing with circuits is performed using [CircuitGraph](https://github.com/circuitgraph/circuitgraph). 13 | 14 | Here's a simple example of locking a basic benchmark circuit. The circuit to lock must be input as a `circuitgraph.Circuit`. The locked circuits is returned as a `circuitgraph.Circuit` and the key is returned as a dictionary mapping key inputs to their correct logical values. 15 | 16 | ```python 17 | import circuitgraph as cg 18 | from logiclocking import locks, write_key 19 | 20 | c = cg.from_lib("c880") 21 | num_keys = 32 22 | cl, k = locks.xor_lock(c, num_keys) 23 | 24 | cg.to_file(cl, "c880_locked.v") 25 | write_key(k, "c880_locked_key.txt") 26 | ``` 27 | 28 | The documentation can be found [here](https://circuitgraph.github.io/logiclocking/). 29 | 30 | ## Installing 31 | 32 | Logiclocking is not yet available on PyPi, so you must install locally. 33 | 34 | ```shell 35 | cd 36 | git clone https://github.com/cmu-actl/logiclocking.git 37 | cd logiclocking 38 | pip install -e . 39 | ``` 40 | 41 | To run the miter attack or use `check_for_difference`, you must install python-sat 42 | 43 | `pip install python-sat` 44 | 45 | If you would like to use the Decision Tree Attack, you must also install sklearn. 46 | 47 | `pip install scikit-learn` 48 | 49 | ## Contributing 50 | 51 | Tests are run using the builtin unittest framework. Some basic linting is performed using flake8. 52 | ```shell 53 | pip instsall flake8 54 | make test 55 | ``` 56 | 57 | Code should be formatted using [black](https://black.readthedocs.io/en/stable/). 58 | [Pre-commit](https://pre-commit.com) is used to automatically run black on commit. 59 | ```shell 60 | pip install black pre-commit 61 | pre-commit install 62 | ``` 63 | Pre-commit also runs a few other hooks, including a docstring formatter and linter. Docs follow the `numpy` documentation convention. 64 | 65 | -------------------------------------------------------------------------------- /docs/templates/config.mako: -------------------------------------------------------------------------------- 1 | <%! 2 | # Template configuration. Copy over in your template directory 3 | # (used with `--template-dir`) and adapt as necessary. 4 | # Note, defaults are loaded from this distribution file, so your 5 | # config.mako only needs to contain values you want overridden. 6 | # You can also run pdoc with `--config KEY=VALUE` to override 7 | # individual values. 8 | 9 | html_lang = 'en' 10 | show_inherited_members = False 11 | extract_module_toc_into_sidebar = True 12 | list_class_variables_in_index = True 13 | sort_identifiers = True 14 | show_type_annotations = True 15 | 16 | # Show collapsed source code block next to each item. 17 | # Disabling this can improve rendering speed of large modules. 18 | show_source_code = True 19 | 20 | # If set, format links to objects in online source code repository 21 | # according to this template. Supported keywords for interpolation 22 | # are: commit, path, start_line, end_line. 23 | #git_link_template = 'https://github.com/USER/PROJECT/blob/{commit}/{path}#L{start_line}-L{end_line}' 24 | #git_link_template = 'https://gitlab.com/USER/PROJECT/blob/{commit}/{path}#L{start_line}-L{end_line}' 25 | #git_link_template = 'https://bitbucket.org/USER/PROJECT/src/{commit}/{path}#lines-{start_line}:{end_line}' 26 | #git_link_template = 'https://CGIT_HOSTNAME/PROJECT/tree/{path}?id={commit}#n{start-line}' 27 | git_link_template = None 28 | 29 | # A prefix to use for every HTML hyperlink in the generated documentation. 30 | # No prefix results in all links being relative. 31 | link_prefix = '' 32 | 33 | # Enable syntax highlighting for code/source blocks by including Highlight.js 34 | syntax_highlighting = True 35 | 36 | # Set the style keyword such as 'atom-one-light' or 'github-gist' 37 | # Options: https://github.com/highlightjs/highlight.js/tree/master/src/styles 38 | # Demo: https://highlightjs.org/static/demo/ 39 | hljs_style = 'github' 40 | 41 | # If set, insert Google Analytics tracking code. Value is GA 42 | # tracking id (UA-XXXXXX-Y). 43 | google_analytics = '' 44 | 45 | # If set, insert Google Custom Search search bar widget above the sidebar index. 46 | # The whitespace-separated tokens represent arbitrary extra queries (at least one 47 | # must match) passed to regular Google search. Example: 48 | #search_query = 'inurl:github.com/USER/PROJECT site:PROJECT.github.io site:PROJECT.website' 49 | search_query = '' 50 | 51 | # If set, render LaTeX math syntax within \(...\) (inline equations), 52 | # or within \[...\] or $$...$$ or `.. math::` (block equations) 53 | # as nicely-formatted math formulas using MathJax. 54 | # Note: in Python docstrings, either all backslashes need to be escaped (\\) 55 | # or you need to use raw r-strings. 56 | latex_math = False 57 | %> 58 | -------------------------------------------------------------------------------- /docs/templates/text.mako: -------------------------------------------------------------------------------- 1 | ## Define mini-templates for each portion of the doco. 2 | 3 | <%! 4 | def indent(s, spaces=4): 5 | new = s.replace('\n', '\n' + ' ' * spaces) 6 | return ' ' * spaces + new.strip() 7 | %> 8 | 9 | <%def name="deflist(s)">:${indent(s)[1:]} 10 | 11 | <%def name="h3(s)">### ${s} 12 | 13 | 14 | <%def name="function(func)" buffered="True"> 15 | <% 16 | returns = show_type_annotations and func.return_annotation() or '' 17 | if returns: 18 | returns = ' \N{non-breaking hyphen}> ' + returns 19 | %> 20 | `${func.name}(${", ".join(func.params(annotate=show_type_annotations))})${returns}` 21 | ${func.docstring | deflist} 22 | 23 | 24 | <%def name="variable(var)" buffered="True"> 25 | <% 26 | annot = show_type_annotations and var.type_annotation() or '' 27 | if annot: 28 | annot = ': ' + annot 29 | %> 30 | `${var.name}${annot}` 31 | ${var.docstring | deflist} 32 | 33 | 34 | <%def name="class_(cls)" buffered="True"> 35 | `${cls.name}(${", ".join(cls.params(annotate=show_type_annotations))})` 36 | ${cls.docstring | deflist} 37 | <% 38 | class_vars = cls.class_variables(show_inherited_members, sort=sort_identifiers) 39 | static_methods = cls.functions(show_inherited_members, sort=sort_identifiers) 40 | inst_vars = cls.instance_variables(show_inherited_members, sort=sort_identifiers) 41 | methods = cls.methods(show_inherited_members, sort=sort_identifiers) 42 | mro = cls.mro() 43 | subclasses = cls.subclasses() 44 | %> 45 | % if mro: 46 | ${h3('Ancestors (in MRO)')} 47 | % for c in mro: 48 | * ${c.refname} 49 | % endfor 50 | 51 | % endif 52 | % if subclasses: 53 | ${h3('Descendants')} 54 | % for c in subclasses: 55 | * ${c.refname} 56 | % endfor 57 | 58 | % endif 59 | % if class_vars: 60 | ${h3('Class variables')} 61 | % for v in class_vars: 62 | ${variable(v) | indent} 63 | 64 | % endfor 65 | % endif 66 | % if static_methods: 67 | ${h3('Static methods')} 68 | % for f in static_methods: 69 | ${function(f) | indent} 70 | 71 | % endfor 72 | % endif 73 | % if inst_vars: 74 | ${h3('Instance variables')} 75 | % for v in inst_vars: 76 | ${variable(v) | indent} 77 | 78 | % endfor 79 | % endif 80 | % if methods: 81 | ${h3('Methods')} 82 | % for m in methods: 83 | ${function(m) | indent} 84 | 85 | % endfor 86 | % endif 87 | 88 | 89 | ## Start the output logic for an entire module. 90 | 91 | <% 92 | variables = module.variables(sort=sort_identifiers) 93 | classes = module.classes(sort=sort_identifiers) 94 | functions = module.functions(sort=sort_identifiers) 95 | submodules = module.submodules() 96 | heading = 'Namespace' if module.is_namespace else 'Module' 97 | %> 98 | 99 | ${heading} ${module.name} 100 | =${'=' * (len(module.name) + len(heading))} 101 | ${module.docstring} 102 | 103 | 104 | % if submodules: 105 | Sub-modules 106 | ----------- 107 | % for m in submodules: 108 | * ${m.name} 109 | % endfor 110 | % endif 111 | 112 | % if variables: 113 | Variables 114 | --------- 115 | % for v in variables: 116 | ${variable(v)} 117 | 118 | % endfor 119 | % endif 120 | 121 | % if functions: 122 | Functions 123 | --------- 124 | % for f in functions: 125 | ${function(f)} 126 | 127 | % endfor 128 | % endif 129 | 130 | % if classes: 131 | Classes 132 | ------- 133 | % for c in classes: 134 | ${class_(c)} 135 | 136 | % endfor 137 | % endif 138 | -------------------------------------------------------------------------------- /logiclocking/utils.py: -------------------------------------------------------------------------------- 1 | """Various utils for working with logic-locked circuits.""" 2 | from ast import literal_eval 3 | 4 | import circuitgraph as cg 5 | 6 | 7 | def check_for_difference(oracle, locked_circuit, key): 8 | """ 9 | Check the correctness of a key. 10 | 11 | Returns any differences between the oracle and a locked circuit 12 | with a specific key applied. 13 | 14 | Parameters 15 | ---------- 16 | oracle: circuitgraph.Circuit 17 | The unlocked circuit to check against. 18 | locked_circuit: circuitgraph.Circuit 19 | The locked circuit to apply the key to. 20 | key: dict of str:bool 21 | The key to check 22 | 23 | Returns 24 | ------- 25 | False or dict of str:bool 26 | False if there is no difference, otherwise the assignment that 27 | produced a difference. 28 | 29 | """ 30 | m = cg.tx.miter(oracle, locked_circuit) 31 | key = {f"c1_{k}": v for k, v in key.items()} 32 | 33 | live = cg.sat.solve(m, assumptions=key) 34 | if not live: 35 | return True 36 | 37 | return cg.sat.solve(m, assumptions={"sat": True, **key}) 38 | 39 | 40 | def locked_unroll( 41 | locked_circuit, 42 | key, 43 | num_copies, 44 | D="D", 45 | Q="Q", 46 | ignore_pins="CK", 47 | initial_values=None, 48 | remove_unloaded=True, 49 | prefix="cg_unroll", 50 | ): 51 | """ 52 | Unrolls a sequential circuit to prepare for a combinational attack. 53 | 54 | This can be used for locks applied on sequential circuits that prevent 55 | scan-chain access. 56 | Note that this function uses the `prefix` variable to identify unrolled 57 | nodes, so choosing a prefix that is already used in nodes in the sequential 58 | circuit can cause undefined behavior. 59 | 60 | Parameters 61 | ---------- 62 | locked_circuit: circuitgraph.Circuit 63 | The circuit to unroll 64 | key: list of str or dict of str:bool 65 | The key inputs. If a dictionary is passed in, this key is used 66 | to construct an unrolled oracle and this is returned in addition 67 | to the unrolled locked circuit 68 | num_copies: int 69 | The number of unrolled copies of the circuit to create 70 | D: str 71 | The name of the D pin of the sequential elements 72 | Q: str 73 | The name of the Q pin of the sequential elements 74 | ignore_pins: str or list of str 75 | The pins on the sequential elements to ignore 76 | initial_values: str or dict of str:str 77 | The initial values of the data ports for the first timestep. 78 | If None, the ports will be added as primary inputs. 79 | If a single value ('0', '1', or 'x'), every flop will get that value. 80 | Can also pass in dict mapping flop names to values. 81 | remove_unloaded: bool 82 | If True, unloaded inputs will be removed after unrolling. This can remove 83 | unused sequential signals such as the clock and reset. 84 | prefix: str 85 | The prefix to use for naming unrolled nodes. 86 | 87 | Returns 88 | ------- 89 | circuitgraph.Circuit, circuitgraph.Circuit, dict of str:list of str 90 | The unrolled locked circuit, the unrolled oracle, and the io map. 91 | 92 | """ 93 | locked_circuit_unrolled, io_map = cg.tx.sequential_unroll( 94 | locked_circuit, 95 | num_copies, 96 | D, 97 | Q, 98 | ignore_pins=ignore_pins, 99 | initial_values=initial_values, 100 | remove_unloaded=remove_unloaded, 101 | prefix=prefix, 102 | ) 103 | 104 | for k in key: 105 | locked_circuit_unrolled.set_type(io_map[k], "buf") 106 | locked_circuit_unrolled.add(k, "input", fanout=io_map[k]) 107 | del io_map[k] 108 | 109 | oracle_unrolled = locked_circuit_unrolled.copy() 110 | for k, v in key.items(): 111 | oracle_unrolled.set_type(k, str(int(v))) 112 | 113 | return locked_circuit_unrolled, oracle_unrolled, io_map 114 | 115 | 116 | def write_key(key, filename): 117 | """Write a key dictionary to a file.""" 118 | with open(filename, "w") as f: 119 | f.write(str(key) + "\n") 120 | 121 | 122 | def read_key(filename): 123 | """Read a key dictionary from a file.""" 124 | with open(filename) as f: 125 | return literal_eval(f.read()) 126 | -------------------------------------------------------------------------------- /tests/test_locks.py: -------------------------------------------------------------------------------- 1 | import random 2 | import unittest 3 | 4 | import circuitgraph as cg 5 | 6 | from logiclocking import locks 7 | from logiclocking.utils import check_for_difference 8 | 9 | 10 | class TestLocks(unittest.TestCase): 11 | def lock_test( 12 | self, 13 | circuit_name, 14 | lock_fn, 15 | lock_args=(), 16 | lock_kwargs={}, 17 | wrong_key_inputs="half", 18 | ): 19 | c = cg.from_lib(circuit_name) 20 | cl, key = lock_fn(c, *lock_args, **lock_kwargs) 21 | cg.lint(cl) 22 | self.assertSetEqual(c.outputs(), cl.outputs()) 23 | self.assertSetEqual(c.inputs(), cl.inputs() - set(key)) 24 | self.assertSetEqual(cl.inputs() - c.inputs(), set(key)) 25 | self.assertFalse(check_for_difference(c, cl, key)) 26 | 27 | wrong_key = key.copy() 28 | if wrong_key_inputs == "one": 29 | inverted_key_input = random.choice(list(key)) 30 | wrong_key[inverted_key_input] = not wrong_key[inverted_key_input] 31 | elif wrong_key_inputs == "half": 32 | for inverted_key_input in random.sample(list(key), len(key) // 2): 33 | wrong_key[inverted_key_input] = not wrong_key[inverted_key_input] 34 | elif wrong_key_inputs == "all": 35 | wrong_key = {k: not v for k, v in key.items()} 36 | else: 37 | raise ValueError(f"Unkown 'wrong_key_inputs' value: '{wrong_key_inputs}'") 38 | self.assertTrue(check_for_difference(c, cl, wrong_key)) 39 | return c, cl, key, wrong_key 40 | 41 | def test_trll(self): 42 | self.lock_test("c880", locks.trll, (32,)) 43 | 44 | def test_xor_lock(self): 45 | self.lock_test("c880", locks.xor_lock, (32,)) 46 | 47 | def test_mux_lock(self): 48 | self.lock_test("c880", locks.mux_lock, (32,)) 49 | 50 | def test_mux_lock_avoid_loops(self): 51 | _, cl, _, _ = self.lock_test( 52 | "c880", locks.mux_lock, (32,), {"avoid_loops": True} 53 | ) 54 | self.assertFalse(cl.is_cyclic()) 55 | 56 | def test_random_lut_lock(self): 57 | self.lock_test("c880", locks.random_lut_lock, (8, 4)) 58 | 59 | def test_lut_lock(self): 60 | self.lock_test("c5315", locks.lut_lock, (100,)) 61 | 62 | def test_lut_lock_not_enough_gates(self): 63 | c = cg.from_lib("c17") 64 | with self.assertRaises(ValueError): 65 | locks.lut_lock(c, 100) 66 | 67 | def test_tt_lock(self): 68 | self.lock_test("c880", locks.tt_lock, (16,)) 69 | 70 | def test_tt_lock_not_enough_inputs(self): 71 | c = cg.from_lib("c17") 72 | with self.assertRaises(ValueError): 73 | locks.tt_lock(c, len(c.inputs()) + 1) 74 | 75 | def test_tt_lock_sen(self): 76 | self.lock_test("c880", locks.tt_lock_sen, (8,)) 77 | 78 | def test_tt_lock_sen_not_enough_inputs(self): 79 | c = cg.from_lib("c17") 80 | with self.assertRaises(ValueError): 81 | locks.tt_lock_sen(c, len(c.inputs()) + 1) 82 | 83 | def test_sfll_hd(self): 84 | self.lock_test("c880", locks.sfll_hd, (16, 4)) 85 | 86 | def test_sfll_hd_not_enough_inputs(self): 87 | c = cg.from_lib("c17") 88 | with self.assertRaises(ValueError): 89 | locks.sfll_hd(c, len(c.inputs()) + 1, 4) 90 | 91 | def test_sfll_flex(self): 92 | self.lock_test("c880", locks.sfll_flex, (16, 4)) 93 | 94 | def test_sfll_flex_not_enough_inputs(self): 95 | c = cg.from_lib("c17") 96 | with self.assertRaises(ValueError): 97 | locks.sfll_flex(c, len(c.inputs()) + 1, 4) 98 | 99 | def test_full_lock(self): 100 | self.lock_test("c499", locks.full_lock, (16, 2)) 101 | 102 | def test_full_lock_avoid_loops(self): 103 | _, cl, _, _ = self.lock_test( 104 | "c880", locks.full_lock, (8, 2), {"avoid_loops": True} 105 | ) 106 | self.assertFalse(cl.is_cyclic()) 107 | 108 | def test_full_lock_mux(self): 109 | self.lock_test("c499", locks.full_lock_mux, (16, 2)) 110 | 111 | def test_inter_lock(self): 112 | bw = 8 113 | _, _, k, _ = self.lock_test("c7552g", locks.inter_lock, (bw,)) 114 | self.assertEqual(len(k), 4 * bw // 2 * 3) 115 | 116 | def test_inter_lock_reduced_swb(self): 117 | bw = 8 118 | _, _, k, _ = self.lock_test( 119 | "c7552g", locks.inter_lock, (bw,), {"reduced_swb": True} 120 | ) 121 | self.assertEqual(len(k), 4 * bw // 2) 122 | 123 | def test_lebl(self): 124 | self.lock_test("c432", locks.lebl, (8, 8)) 125 | -------------------------------------------------------------------------------- /docs/templates/pdf.mako: -------------------------------------------------------------------------------- 1 | <% 2 | import re 3 | import pdoc 4 | from pdoc.html_helpers import to_markdown 5 | 6 | def link(dobj: pdoc.Doc): 7 | name = dobj.qualname + ('()' if isinstance(dobj, pdoc.Function) else '') 8 | if isinstance(dobj, pdoc.External): 9 | return name 10 | return '[{0}](#{1} "{1}")'.format(name, dobj.refname) 11 | 12 | def _to_md(text, module): 13 | text = to_markdown(text, docformat=docformat, module=module, link=link) 14 | # Setext H2 headings to atx H2 headings 15 | text = re.sub(r'\n(.+)\n-{3,}\n', r'\n## \1\n\n', text) 16 | # Convert admonitions into simpler paragraphs, dedent contents 17 | text = re.sub(r'^(?P( *))!!! \w+ \"([^\"]*)\"(.*(?:\n(?P=indent) +.*)*)', 18 | lambda m: '{}**{}:** {}'.format(m.group(2), m.group(3), 19 | re.sub('\n {,4}', '\n', m.group(4))), 20 | text, flags=re.MULTILINE) 21 | return text 22 | 23 | def subh(text, level=2): 24 | # Deepen heading levels so H2 becomes H4 etc. 25 | return re.sub(r'\n(#+) +(.+)\n', r'\n%s\1 \2\n' % ('#' * level), text) 26 | %> 27 | 28 | <%def name="title(level, string, id=None)"> 29 | <% id = ' {#%s}' % id if id is not None else '' %> 30 | ${('#' * level) + ' ' + string + id} 31 | 32 | 33 | <%def name="funcdef(f)"> 34 | <% 35 | params = f.params(annotate=show_type_annotations) 36 | returns = show_type_annotations and f.return_annotation() or '' 37 | if returns: 38 | returns = ' \N{non-breaking hyphen}> ' + returns 39 | %> 40 | %if params: 41 | > ${f.funcdef()} ${f.name}( 42 | > ${',\n> '.join(params)} 43 | > )${returns} 44 | %else: 45 | > ${f.funcdef()} ${f.name}()${returns} 46 | %endif 47 | 48 | 49 | <%def name="classdef(c)"> 50 | <% params = c.params(annotate=show_type_annotations) %> 51 | %if params: 52 | > class ${c.name}( 53 | > ${',\n> '.join(params)} 54 | > ) 55 | %else: 56 | > class ${c.name} 57 | %endif 58 | 59 | 60 | <%def name="vartype(v)"> 61 | <% annot = show_type_annotations and v.type_annotation() or '' %> 62 | %if annot: 63 | Type: `${annot}` 64 | %endif 65 | 66 | 67 | --- 68 | description: | 69 | API documentation for modules: ${', '.join(m.name for m in modules)}. 70 | 71 | lang: en 72 | 73 | classoption: oneside 74 | geometry: margin=1in 75 | papersize: a4 76 | 77 | linkcolor: blue 78 | links-as-notes: true 79 | ... 80 | % for module in modules: 81 | <% 82 | submodules = module.submodules() 83 | variables = module.variables(sort=sort_identifiers) 84 | functions = module.functions(sort=sort_identifiers) 85 | classes = module.classes(sort=sort_identifiers) 86 | 87 | def to_md(text): 88 | return _to_md(text, module) 89 | %> 90 | ${title(1, ('Namespace' if module.is_namespace else 'Module') + ' `%s`' % module.name, module.refname)} 91 | ${module.docstring | to_md} 92 | 93 | % if submodules: 94 | ${title(2, 'Sub-modules')} 95 | % for m in submodules: 96 | * [${m.name}](#${m.refname}) 97 | % endfor 98 | % endif 99 | 100 | % if variables: 101 | ${title(2, 'Variables')} 102 | % for v in variables: 103 | ${title(3, 'Variable `%s`' % v.name, v.refname)} 104 | ${vartype(v)} 105 | ${v.docstring | to_md, subh, subh} 106 | % endfor 107 | % endif 108 | 109 | % if functions: 110 | ${title(2, 'Functions')} 111 | % for f in functions: 112 | ${title(3, 'Function `%s`' % f.name, f.refname)} 113 | 114 | ${funcdef(f)} 115 | 116 | ${f.docstring | to_md, subh, subh} 117 | % endfor 118 | % endif 119 | 120 | % if classes: 121 | ${title(2, 'Classes')} 122 | % for cls in classes: 123 | ${title(3, 'Class `%s`' % cls.name, cls.refname)} 124 | 125 | ${classdef(cls)} 126 | 127 | ${cls.docstring | to_md, subh} 128 | <% 129 | class_vars = cls.class_variables(show_inherited_members, sort=sort_identifiers) 130 | static_methods = cls.functions(show_inherited_members, sort=sort_identifiers) 131 | inst_vars = cls.instance_variables(show_inherited_members, sort=sort_identifiers) 132 | methods = cls.methods(show_inherited_members, sort=sort_identifiers) 133 | mro = cls.mro() 134 | subclasses = cls.subclasses() 135 | %> 136 | % if mro: 137 | ${title(4, 'Ancestors (in MRO)')} 138 | % for c in mro: 139 | * [${c.refname}](#${c.refname}) 140 | % endfor 141 | % endif 142 | 143 | % if subclasses: 144 | ${title(4, 'Descendants')} 145 | % for c in subclasses: 146 | * [${c.refname}](#${c.refname}) 147 | % endfor 148 | % endif 149 | 150 | % if class_vars: 151 | ${title(4, 'Class variables')} 152 | % for v in class_vars: 153 | ${title(5, 'Variable `%s`' % v.name, v.refname)} 154 | ${vartype(v)} 155 | ${v.docstring | to_md, subh, subh} 156 | % endfor 157 | % endif 158 | 159 | % if inst_vars: 160 | ${title(4, 'Instance variables')} 161 | % for v in inst_vars: 162 | ${title(5, 'Variable `%s`' % v.name, v.refname)} 163 | ${vartype(v)} 164 | ${v.docstring | to_md, subh, subh} 165 | % endfor 166 | % endif 167 | 168 | % if static_methods: 169 | ${title(4, 'Static methods')} 170 | % for f in static_methods: 171 | ${title(5, '`Method %s`' % f.name, f.refname)} 172 | 173 | ${funcdef(f)} 174 | 175 | ${f.docstring | to_md, subh, subh} 176 | % endfor 177 | % endif 178 | 179 | % if methods: 180 | ${title(4, 'Methods')} 181 | % for f in methods: 182 | ${title(5, 'Method `%s`' % f.name, f.refname)} 183 | 184 | ${funcdef(f)} 185 | 186 | ${f.docstring | to_md, subh, subh} 187 | % endfor 188 | % endif 189 | % endfor 190 | % endif 191 | 192 | ##\## for module in modules: 193 | % endfor 194 | 195 | ----- 196 | Generated by *pdoc* ${pdoc.__version__} (). 197 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | logiclocking API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |

Package logiclocking

23 |
24 |
25 |

Implementations of logic locking techniques and attacks.

26 |
27 | 28 | Expand source code 29 | 30 |
"""Implementations of logic locking techniques and attacks."""
31 | 
32 | from logiclocking.utils import (
33 |     check_for_difference,
34 |     locked_unroll,
35 |     read_key,
36 |     write_key,
37 | )
38 |
39 |
40 |
41 |

Sub-modules

42 |
43 |
logiclocking.attacks
44 |
45 |

Run attacks on logic-locked circuits.

46 |
47 |
logiclocking.locks
48 |
49 |

Apply logic locks to circuits.

50 |
51 |
logiclocking.metrics
52 |
53 |

Cacluate logic-locking metrics.

54 |
55 |
logiclocking.utils
56 |
57 |

Various utils for working with logic-locked circuits.

58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 | 88 |
89 | 92 | 93 | -------------------------------------------------------------------------------- /docs/templates/css.mako: -------------------------------------------------------------------------------- 1 | <%! 2 | from pdoc.html_helpers import minify_css 3 | %> 4 | 5 | <%def name="mobile()" filter="minify_css"> 6 | :root { 7 | --highlight-color: #fe9; 8 | } 9 | .flex { 10 | display: flex !important; 11 | } 12 | 13 | body { 14 | line-height: 1.5em; 15 | } 16 | 17 | #content { 18 | padding: 20px; 19 | } 20 | 21 | #sidebar { 22 | padding: 30px; 23 | overflow: hidden; 24 | } 25 | #sidebar > *:last-child { 26 | margin-bottom: 2cm; 27 | } 28 | 29 | .http-server-breadcrumbs { 30 | font-size: 130%; 31 | margin: 0 0 15px 0; 32 | } 33 | 34 | #footer { 35 | font-size: .75em; 36 | padding: 5px 30px; 37 | border-top: 1px solid #ddd; 38 | text-align: right; 39 | } 40 | #footer p { 41 | margin: 0 0 0 1em; 42 | display: inline-block; 43 | } 44 | #footer p:last-child { 45 | margin-right: 30px; 46 | } 47 | 48 | h1, h2, h3, h4, h5 { 49 | font-weight: 300; 50 | } 51 | h1 { 52 | font-size: 2.5em; 53 | line-height: 1.1em; 54 | } 55 | h2 { 56 | font-size: 1.75em; 57 | margin: 1em 0 .50em 0; 58 | } 59 | h3 { 60 | font-size: 1.4em; 61 | margin: 25px 0 10px 0; 62 | } 63 | h4 { 64 | margin: 0; 65 | font-size: 105%; 66 | } 67 | h1:target, 68 | h2:target, 69 | h3:target, 70 | h4:target, 71 | h5:target, 72 | h6:target { 73 | background: var(--highlight-color); 74 | padding: .2em 0; 75 | } 76 | 77 | a { 78 | color: #058; 79 | text-decoration: none; 80 | transition: color .3s ease-in-out; 81 | } 82 | a:hover { 83 | color: #e82; 84 | } 85 | 86 | .title code { 87 | font-weight: bold; 88 | } 89 | h2[id^="header-"] { 90 | margin-top: 2em; 91 | } 92 | .ident { 93 | color: #900; 94 | } 95 | 96 | pre code { 97 | background: #f8f8f8; 98 | font-size: .8em; 99 | line-height: 1.4em; 100 | } 101 | code { 102 | background: #f2f2f1; 103 | padding: 1px 4px; 104 | overflow-wrap: break-word; 105 | } 106 | h1 code { background: transparent } 107 | 108 | pre { 109 | background: #f8f8f8; 110 | border: 0; 111 | border-top: 1px solid #ccc; 112 | border-bottom: 1px solid #ccc; 113 | margin: 1em 0; 114 | padding: 1ex; 115 | } 116 | 117 | #http-server-module-list { 118 | display: flex; 119 | flex-flow: column; 120 | } 121 | #http-server-module-list div { 122 | display: flex; 123 | } 124 | #http-server-module-list dt { 125 | min-width: 10%; 126 | } 127 | #http-server-module-list p { 128 | margin-top: 0; 129 | } 130 | 131 | .toc ul, 132 | #index { 133 | list-style-type: none; 134 | margin: 0; 135 | padding: 0; 136 | } 137 | #index code { 138 | background: transparent; 139 | } 140 | #index h3 { 141 | border-bottom: 1px solid #ddd; 142 | } 143 | #index ul { 144 | padding: 0; 145 | } 146 | #index h4 { 147 | margin-top: .6em; 148 | font-weight: bold; 149 | } 150 | /* Make TOC lists have 2+ columns when viewport is wide enough. 151 | Assuming ~20-character identifiers and ~30% wide sidebar. */ 152 | @media (min-width: 200ex) { #index .two-column { column-count: 2 } } 153 | @media (min-width: 300ex) { #index .two-column { column-count: 3 } } 154 | 155 | dl { 156 | margin-bottom: 2em; 157 | } 158 | dl dl:last-child { 159 | margin-bottom: 4em; 160 | } 161 | dd { 162 | margin: 0 0 1em 3em; 163 | } 164 | #header-classes + dl > dd { 165 | margin-bottom: 3em; 166 | } 167 | dd dd { 168 | margin-left: 2em; 169 | } 170 | dd p { 171 | margin: 10px 0; 172 | } 173 | .name { 174 | background: #eee; 175 | font-weight: bold; 176 | font-size: .85em; 177 | padding: 5px 10px; 178 | display: inline-block; 179 | min-width: 40%; 180 | } 181 | .name:hover { 182 | background: #e0e0e0; 183 | } 184 | dt:target .name { 185 | background: var(--highlight-color); 186 | } 187 | .name > span:first-child { 188 | white-space: nowrap; 189 | } 190 | .name.class > span:nth-child(2) { 191 | margin-left: .4em; 192 | } 193 | .inherited { 194 | color: #999; 195 | border-left: 5px solid #eee; 196 | padding-left: 1em; 197 | } 198 | .inheritance em { 199 | font-style: normal; 200 | font-weight: bold; 201 | } 202 | 203 | /* Docstrings titles, e.g. in numpydoc format */ 204 | .desc h2 { 205 | font-weight: 400; 206 | font-size: 1.25em; 207 | } 208 | .desc h3 { 209 | font-size: 1em; 210 | } 211 | .desc dt code { 212 | background: inherit; /* Don't grey-back parameters */ 213 | } 214 | 215 | .source summary, 216 | .git-link-div { 217 | color: #666; 218 | text-align: right; 219 | font-weight: 400; 220 | font-size: .8em; 221 | text-transform: uppercase; 222 | } 223 | .source summary > * { 224 | white-space: nowrap; 225 | cursor: pointer; 226 | } 227 | .git-link { 228 | color: inherit; 229 | margin-left: 1em; 230 | } 231 | .source pre { 232 | max-height: 500px; 233 | overflow: auto; 234 | margin: 0; 235 | } 236 | .source pre code { 237 | font-size: 12px; 238 | overflow: visible; 239 | } 240 | .hlist { 241 | list-style: none; 242 | } 243 | .hlist li { 244 | display: inline; 245 | } 246 | .hlist li:after { 247 | content: ',\2002'; 248 | } 249 | .hlist li:last-child:after { 250 | content: none; 251 | } 252 | .hlist .hlist { 253 | display: inline; 254 | padding-left: 1em; 255 | } 256 | 257 | img { 258 | max-width: 100%; 259 | } 260 | td { 261 | padding: 0 .5em; 262 | } 263 | 264 | .admonition { 265 | padding: .1em .5em; 266 | margin-bottom: 1em; 267 | } 268 | .admonition-title { 269 | font-weight: bold; 270 | } 271 | .admonition.note, 272 | .admonition.info, 273 | .admonition.important { 274 | background: #aef; 275 | } 276 | .admonition.todo, 277 | .admonition.versionadded, 278 | .admonition.tip, 279 | .admonition.hint { 280 | background: #dfd; 281 | } 282 | .admonition.warning, 283 | .admonition.versionchanged, 284 | .admonition.deprecated { 285 | background: #fd4; 286 | } 287 | .admonition.error, 288 | .admonition.danger, 289 | .admonition.caution { 290 | background: lightpink; 291 | } 292 | 293 | 294 | <%def name="desktop()" filter="minify_css"> 295 | @media screen and (min-width: 700px) { 296 | #sidebar { 297 | width: 30%; 298 | height: 100vh; 299 | overflow: auto; 300 | position: sticky; 301 | top: 0; 302 | } 303 | #content { 304 | width: 70%; 305 | max-width: 100ch; 306 | padding: 3em 4em; 307 | border-left: 1px solid #ddd; 308 | } 309 | pre code { 310 | font-size: 1em; 311 | } 312 | .item .name { 313 | font-size: 1em; 314 | } 315 | main { 316 | display: flex; 317 | flex-direction: row-reverse; 318 | justify-content: flex-end; 319 | } 320 | .toc ul ul, 321 | #index ul { 322 | padding-left: 1.5em; 323 | } 324 | .toc > ul > li { 325 | margin-top: .5em; 326 | } 327 | } 328 | 329 | 330 | <%def name="print()" filter="minify_css"> 331 | @media print { 332 | #sidebar h1 { 333 | page-break-before: always; 334 | } 335 | .source { 336 | display: none; 337 | } 338 | } 339 | @media print { 340 | * { 341 | background: transparent !important; 342 | color: #000 !important; /* Black prints faster: h5bp.com/s */ 343 | box-shadow: none !important; 344 | text-shadow: none !important; 345 | } 346 | 347 | a[href]:after { 348 | content: " (" attr(href) ")"; 349 | font-size: 90%; 350 | } 351 | /* Internal, documentation links, recognized by having a title, 352 | don't need the URL explicity stated. */ 353 | a[href][title]:after { 354 | content: none; 355 | } 356 | 357 | abbr[title]:after { 358 | content: " (" attr(title) ")"; 359 | } 360 | 361 | /* 362 | * Don't show links for images, or javascript/internal links 363 | */ 364 | 365 | .ir a:after, 366 | a[href^="javascript:"]:after, 367 | a[href^="#"]:after { 368 | content: ""; 369 | } 370 | 371 | pre, 372 | blockquote { 373 | border: 1px solid #999; 374 | page-break-inside: avoid; 375 | } 376 | 377 | thead { 378 | display: table-header-group; /* h5bp.com/t */ 379 | } 380 | 381 | tr, 382 | img { 383 | page-break-inside: avoid; 384 | } 385 | 386 | img { 387 | max-width: 100% !important; 388 | } 389 | 390 | @page { 391 | margin: 0.5cm; 392 | } 393 | 394 | p, 395 | h2, 396 | h3 { 397 | orphans: 3; 398 | widows: 3; 399 | } 400 | 401 | h1, 402 | h2, 403 | h3, 404 | h4, 405 | h5, 406 | h6 { 407 | page-break-after: avoid; 408 | } 409 | } 410 | 411 | -------------------------------------------------------------------------------- /logiclocking/attacks.py: -------------------------------------------------------------------------------- 1 | """Run attacks on logic-locked circuits.""" 2 | import code 3 | import random 4 | import time 5 | 6 | import circuitgraph as cg 7 | 8 | 9 | def _localtime(): 10 | return time.asctime(time.localtime(time.time())) 11 | 12 | 13 | def miter_attack( 14 | cl, 15 | key, 16 | timeout=None, 17 | key_cons=None, 18 | unroll_cyclic=True, 19 | verbose=True, 20 | code_on_error=False, 21 | ): 22 | """ 23 | Launch a miter-based sat attack on a locked circuit. 24 | 25 | Parameters 26 | ---------- 27 | cl: circuitgraph.Circuit 28 | The locked circuit to attack 29 | key: dict of str:bool 30 | The correct key, used to construct the oracle 31 | timeout: int 32 | Timeout for the attack, in seconds 33 | key_cons: circuitgraph.Circuit or iter of circuitgraph.Circuit 34 | Key conditions to satisfy during attack, 35 | must have output 'sat' and be a function of the key inputs 36 | unroll_cyclic: bool 37 | If True, convert cyclic circuits to acyclic versions 38 | verbose: bool 39 | If True, attack progress will be printed 40 | code_on_error: bool 41 | If True, drop into an interactive session on an error 42 | 43 | Returns 44 | ------- 45 | dict 46 | A dictionary containing attack info and results 47 | 48 | """ 49 | start_time = time.time() 50 | 51 | if cl.is_cyclic(): 52 | if unroll_cyclic: 53 | cl = cg.tx.acyclic_unroll(cl) 54 | else: 55 | raise ValueError( 56 | "Circuit is cyclic. Set 'unroll_cyclic' to True to run sat on " 57 | "this circuit" 58 | ) 59 | 60 | # setup vars 61 | keys = tuple(key.keys()) 62 | ins = tuple(cl.startpoints() - key.keys()) 63 | outs = tuple(cl.endpoints()) 64 | 65 | # create simulation solver 66 | s_sim, v_sim = cg.sat.construct_solver(cl, key) 67 | 68 | # create miter solver 69 | m = cg.tx.miter(cl, startpoints=set(ins)) 70 | s_miter, v_miter = cg.sat.construct_solver(m) 71 | 72 | # add key constraints 73 | if key_cons: 74 | if isinstance(key_cons, cg.Circuit): 75 | key_cons = [key_cons] 76 | for key_con in key_cons: 77 | if verbose: 78 | print( 79 | f"[{_localtime()}] circuit: {cl.name}, " 80 | f"adding constraints: {key_con.name}" 81 | ) 82 | formula, v_cons = cg.sat.cnf(key_con) 83 | con_clauses = formula.clauses 84 | 85 | # add constraints circuits 86 | c0_offset = s_miter.nof_vars() 87 | c0 = cg.sat.remap(con_clauses, c0_offset) 88 | s_miter.append_formula(c0) 89 | c1_offset = s_miter.nof_vars() 90 | c1 = cg.sat.remap(con_clauses, c1_offset) 91 | s_miter.append_formula(c1) 92 | 93 | # encode keys connections 94 | clauses = [[v_cons.id("sat") + c0_offset], [v_cons.id("sat") + c1_offset]] 95 | clauses += [ 96 | [-v_miter.id(f"c0_{n}"), v_cons.id(n) + c0_offset] for n in keys 97 | ] 98 | clauses += [ 99 | [v_miter.id(f"c0_{n}"), -v_cons.id(n) - c0_offset] for n in keys 100 | ] 101 | clauses += [ 102 | [-v_miter.id(f"c1_{n}"), v_cons.id(n) + c1_offset] for n in keys 103 | ] 104 | clauses += [ 105 | [v_miter.id(f"c1_{n}"), -v_cons.id(n) - c1_offset] for n in keys 106 | ] 107 | 108 | s_miter.append_formula(clauses) 109 | 110 | # get circuit clauses 111 | formula, v_c = cg.sat.cnf(cl) 112 | clauses = formula.clauses 113 | 114 | # solve 115 | dis = [] 116 | dos = [] 117 | iter_times = [] 118 | iter_keys = [] 119 | while s_miter.solve(assumptions=[v_miter.id("sat")]): 120 | 121 | # get di 122 | model = s_miter.get_model() 123 | di = [model[v_miter.id(n) - 1] > 0 for n in ins] 124 | if tuple(di) in dis: 125 | if code_on_error: 126 | print("Error di") 127 | code.interact(local=dict(globals(), **locals())) 128 | else: 129 | raise ValueError("Saw same di twice") 130 | 131 | # get intermediate keys 132 | k0 = {n: model[v_miter.id(f"c0_{n}") - 1] > 0 for n in keys} 133 | k1 = {n: model[v_miter.id(f"c1_{n}") - 1] > 0 for n in keys} 134 | iter_keys.append((k0, k1)) 135 | 136 | # get do 137 | s_sim.solve(assumptions=[(2 * b - 1) * v_sim.id(n) for b, n in zip(di, ins)]) 138 | model = s_sim.get_model() 139 | if model is None: 140 | if code_on_error: 141 | print("Error sim") 142 | code.interact(local=dict(globals(), **locals())) 143 | else: 144 | raise ValueError("Could not get simulation model") 145 | do = [model[v_sim.id(n) - 1] > 0 for n in outs] 146 | dis.append(tuple(di)) 147 | dos.append(tuple(do)) 148 | iter_times.append(time.time() - start_time) 149 | 150 | # add constraints circuits 151 | c0_offset = s_miter.nof_vars() 152 | c0 = cg.sat.remap(clauses, c0_offset) 153 | s_miter.append_formula(c0) 154 | c1_offset = s_miter.nof_vars() 155 | c1 = cg.sat.remap(clauses, c1_offset) 156 | s_miter.append_formula(c1) 157 | 158 | # encode dis + dos 159 | dio_clauses = [ 160 | [(2 * b - 1) * (v_c.id(n) + c0_offset)] for b, n in zip(di + do, ins + outs) 161 | ] 162 | dio_clauses += [ 163 | [(2 * b - 1) * (v_c.id(n) + c1_offset)] for b, n in zip(di + do, ins + outs) 164 | ] 165 | s_miter.append_formula(dio_clauses) 166 | 167 | # encode keys connections 168 | key_clauses = [[-v_miter.id(f"c0_{n}"), v_c.id(n) + c0_offset] for n in keys] 169 | key_clauses += [[v_miter.id(f"c0_{n}"), -v_c.id(n) - c0_offset] for n in keys] 170 | key_clauses += [[-v_miter.id(f"c1_{n}"), v_c.id(n) + c1_offset] for n in keys] 171 | key_clauses += [[v_miter.id(f"c1_{n}"), -v_c.id(n) - c1_offset] for n in keys] 172 | s_miter.append_formula(key_clauses) 173 | 174 | # check timeout 175 | if timeout and (time.time() - start_time) > timeout: 176 | print(f"[{_localtime()}] circuit: {cl.name}, Timeout: True") 177 | return { 178 | "Time": None, 179 | "Iterations": len(dis), 180 | "Timeout": True, 181 | "Equivalent": False, 182 | "Key Found": False, 183 | "dis": dis, 184 | "dos": dos, 185 | "iter_times": iter_times, 186 | "iter_keys": iter_keys, 187 | } 188 | 189 | if verbose: 190 | print( 191 | f"[{_localtime()}] " 192 | f"circuit: {cl.name}, iter: {len(dis)}, " 193 | f"time: {time.time()-start_time}, " 194 | f"clauses: {s_miter.nof_clauses()}, " 195 | f"vars: {s_miter.nof_vars()}" 196 | ) 197 | 198 | # check if a satisfying key remains 199 | key_found = s_miter.solve() 200 | if verbose: 201 | print(f"[{_localtime()}] circuit: {cl.name}, key found: {key_found}") 202 | if not key_found: 203 | return { 204 | "Time": None, 205 | "Iterations": len(dis), 206 | "Timeout": False, 207 | "Equivalent": False, 208 | "Key Found": False, 209 | "dis": dis, 210 | "dos": dos, 211 | "iter_times": iter_times, 212 | "iter_keys": iter_keys, 213 | } 214 | 215 | # get key 216 | model = s_miter.get_model() 217 | attack_key = {n: model[v_miter.id(f"c1_{n}") - 1] > 0 for n in keys} 218 | 219 | # check key 220 | assumptions = { 221 | **{f"c0_{k}": v for k, v in key.items()}, 222 | **{f"c1_{k}": v for k, v in attack_key.items()}, 223 | "sat": True, 224 | } 225 | equivalent = not cg.sat.solve(m, assumptions) 226 | if verbose: 227 | print(f"[{_localtime()}] circuit: {cl.name}, equivalent: {equivalent}") 228 | 229 | exec_time = time.time() - start_time 230 | if verbose: 231 | print(f"[{_localtime()}] circuit: {cl.name}, elasped time: {exec_time}") 232 | 233 | return { 234 | "Time": exec_time, 235 | "Iterations": len(dis), 236 | "Timeout": False, 237 | "Equivalent": equivalent, 238 | "Key Found": True, 239 | "dis": dis, 240 | "dos": dos, 241 | "iter_times": iter_times, 242 | "iter_keys": iter_keys, 243 | "attack_key": attack_key, 244 | } 245 | 246 | 247 | def decision_tree_attack(c_or_cl, nsamples, key=None, verbose=True): 248 | """ 249 | Launch a decision tree attack on a locked circuit. 250 | 251 | Attempts to capture the functionality of the oracle circuit using a 252 | decision tree. 253 | 254 | Paramters 255 | --------- 256 | c_or_cl: circuitgraph.Circuit 257 | The circuit to reverse engineer. Can either be 258 | the oracle or the locked circuit. If the locked 259 | circuit, must pass in the correct key using 260 | the `key` parameter 261 | nsamples: int 262 | The number of samples to train the decision tree on 263 | key: dict of str:bool 264 | The correct key, used to construct the oracle if 265 | the locked circuit is given. 266 | verbose: bool 267 | If True, attack progress will be printed 268 | 269 | Returns 270 | ------- 271 | dict of str:sklearn.tree.DecisionTreeClassifier 272 | The trained classifier for each output. 273 | 274 | """ 275 | from sklearn.tree import DecisionTreeClassifier 276 | 277 | if key: 278 | cl = c_or_cl 279 | for k, v in key.items(): 280 | cl.set_type(k, str(int(v))) 281 | c = cl 282 | else: 283 | c = c_or_cl 284 | 285 | ins = tuple(c.startpoints()) 286 | outs = tuple(c.endpoints()) 287 | 288 | # generate training samples 289 | x = [] 290 | y = {o: [] for o in outs} 291 | if verbose: 292 | print(f"[{_localtime()}] Generating samples") 293 | for i in range(nsamples): 294 | x += [[random.choice((True, False)) for i in ins]] 295 | result = cg.sat.solve(c, {i: v for i, v in zip(ins, x[-1])}) 296 | for o in outs: 297 | y[o] += [result[o]] 298 | 299 | if verbose: 300 | print(f"[{_localtime()}] Training decision trees") 301 | estimators = {o: DecisionTreeClassifier() for o in outs} 302 | for o in outs: 303 | estimators[o].fit(x, y[o]) 304 | 305 | if verbose: 306 | print(f"[{_localtime()}] Testing decision trees") 307 | ncorrect = 0 308 | for i in range(nsamples): 309 | x = [[random.choice((True, False)) for i in ins]] 310 | result = cg.sat.solve(c, {i: v for i, v in zip(ins, x[-1])}) 311 | if all(result[o] == estimators[o].predict(x) for o in outs): 312 | ncorrect += 1 313 | 314 | if verbose: 315 | print(f"[{_localtime()}] Test accuracy: {ncorrect / nsamples}") 316 | return estimators 317 | -------------------------------------------------------------------------------- /docs/metrics.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | logiclocking.metrics API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |

Module logiclocking.metrics

23 |
24 |
25 |

Cacluate logic-locking metrics.

26 |
27 | 28 | Expand source code 29 | 30 |
"""Cacluate logic-locking metrics."""
 31 | from random import random
 32 | from statistics import mean
 33 | 
 34 | import circuitgraph as cg
 35 | 
 36 | 
 37 | def corruptibility(cl, key):
 38 |     """Apprixmate corruptability of a locked circuit for a specific key."""
 39 |     # set up miter
 40 |     ins = set(cl.startpoints() - key.keys())
 41 |     m = cg.miter(cl, startpoints=ins)
 42 |     set_key = {f"c0_{k}": v for k, v in key.items()}
 43 |     independent_set = {f"c1_{k}" for k in key} | ins
 44 | 
 45 |     # run approx
 46 |     errors = cg.approx_model_count(m, {**set_key, "sat": True}, independent_set)
 47 | 
 48 |     return errors / 2 ** len(independent_set)
 49 | 
 50 | 
 51 | def key_corruption(cl, key, attack_key):
 52 |     """Approximate corruption between two keys."""
 53 |     # set up miter
 54 |     ins = set(cl.startpoints() - key.keys())
 55 |     m = cg.miter(cl, startpoints=ins)
 56 |     c0_key = {f"c0_{k}": v for k, v in key.items()}
 57 |     c1_key = {f"c1_{k}": v for k, v in attack_key.items()}
 58 | 
 59 |     # run approx
 60 |     errors = cg.approx_model_count(m, {**c0_key, **c1_key, "sat": True}, ins)
 61 | 
 62 |     return errors / 2 ** len(ins)
 63 | 
 64 | 
 65 | def min_corruption(cl, key, e=0.1, min_samples=10, tol=0.1):
 66 |     """Approximate the minimum corruption."""
 67 |     # find total errors
 68 |     cor = corruptibility(cl, key)
 69 | 
 70 |     # get initial key corruptions
 71 |     key_corruptions = []
 72 |     for i in range(min_samples):
 73 |         sampled_key = {k: random() < 0.5 for k in key}
 74 |         key_corruptions.append(key_corruption(cl, key, sampled_key))
 75 | 
 76 |     # sample until distribution matches
 77 |     while abs(mean(key_corruptions) - cor) > tol:
 78 |         sampled_key = {k: random() < 0.5 for k in key}
 79 |         key_corruptions.append(key_corruption(cl, key, sampled_key))
 80 | 
 81 |     return len([kc for kc in key_corruptions if kc >= e]) / len(key_corruptions)
 82 | 
 83 | 
 84 | def avg_avg_sensitivity(cl, key={}):
 85 |     """Get the average average sensitivity under a given key."""
 86 |     # set key
 87 |     cl_key = cl.copy()
 88 |     for k, v in key.items():
 89 |         cl_key.set_type(k, v)
 90 | 
 91 |     avg_sens = []
 92 |     for o in cl.outputs():
 93 |         # estimate sen
 94 |         avg_sens.append(cl.avg_sensitivity(o, e=3, d=0.8) / len(cl.startpoints(o)))
 95 | 
 96 |     return mean(avg_sens)
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |

Functions

105 |
106 |
107 | def avg_avg_sensitivity(cl, key={}) 108 |
109 |
110 |

Get the average average sensitivity under a given key.

111 |
112 | 113 | Expand source code 114 | 115 |
def avg_avg_sensitivity(cl, key={}):
116 |     """Get the average average sensitivity under a given key."""
117 |     # set key
118 |     cl_key = cl.copy()
119 |     for k, v in key.items():
120 |         cl_key.set_type(k, v)
121 | 
122 |     avg_sens = []
123 |     for o in cl.outputs():
124 |         # estimate sen
125 |         avg_sens.append(cl.avg_sensitivity(o, e=3, d=0.8) / len(cl.startpoints(o)))
126 | 
127 |     return mean(avg_sens)
128 |
129 |
130 |
131 | def corruptibility(cl, key) 132 |
133 |
134 |

Apprixmate corruptability of a locked circuit for a specific key.

135 |
136 | 137 | Expand source code 138 | 139 |
def corruptibility(cl, key):
140 |     """Apprixmate corruptability of a locked circuit for a specific key."""
141 |     # set up miter
142 |     ins = set(cl.startpoints() - key.keys())
143 |     m = cg.miter(cl, startpoints=ins)
144 |     set_key = {f"c0_{k}": v for k, v in key.items()}
145 |     independent_set = {f"c1_{k}" for k in key} | ins
146 | 
147 |     # run approx
148 |     errors = cg.approx_model_count(m, {**set_key, "sat": True}, independent_set)
149 | 
150 |     return errors / 2 ** len(independent_set)
151 |
152 |
153 |
154 | def key_corruption(cl, key, attack_key) 155 |
156 |
157 |

Approximate corruption between two keys.

158 |
159 | 160 | Expand source code 161 | 162 |
def key_corruption(cl, key, attack_key):
163 |     """Approximate corruption between two keys."""
164 |     # set up miter
165 |     ins = set(cl.startpoints() - key.keys())
166 |     m = cg.miter(cl, startpoints=ins)
167 |     c0_key = {f"c0_{k}": v for k, v in key.items()}
168 |     c1_key = {f"c1_{k}": v for k, v in attack_key.items()}
169 | 
170 |     # run approx
171 |     errors = cg.approx_model_count(m, {**c0_key, **c1_key, "sat": True}, ins)
172 | 
173 |     return errors / 2 ** len(ins)
174 |
175 |
176 |
177 | def min_corruption(cl, key, e=0.1, min_samples=10, tol=0.1) 178 |
179 |
180 |

Approximate the minimum corruption.

181 |
182 | 183 | Expand source code 184 | 185 |
def min_corruption(cl, key, e=0.1, min_samples=10, tol=0.1):
186 |     """Approximate the minimum corruption."""
187 |     # find total errors
188 |     cor = corruptibility(cl, key)
189 | 
190 |     # get initial key corruptions
191 |     key_corruptions = []
192 |     for i in range(min_samples):
193 |         sampled_key = {k: random() < 0.5 for k in key}
194 |         key_corruptions.append(key_corruption(cl, key, sampled_key))
195 | 
196 |     # sample until distribution matches
197 |     while abs(mean(key_corruptions) - cor) > tol:
198 |         sampled_key = {k: random() < 0.5 for k in key}
199 |         key_corruptions.append(key_corruption(cl, key, sampled_key))
200 | 
201 |     return len([kc for kc in key_corruptions if kc >= e]) / len(key_corruptions)
202 |
203 |
204 |
205 | def random() 206 |
207 |
208 |

random() -> x in the interval [0, 1).

209 |
210 |
211 |
212 |
213 |
214 |
215 | 241 |
242 | 245 | 246 | -------------------------------------------------------------------------------- /docs/templates/html.mako: -------------------------------------------------------------------------------- 1 | <% 2 | import os 3 | 4 | import pdoc 5 | from pdoc.html_helpers import extract_toc, glimpse, to_html as _to_html, format_git_link 6 | 7 | 8 | def link(dobj: pdoc.Doc, name=None): 9 | name = name or dobj.qualname + ('()' if isinstance(dobj, pdoc.Function) else '') 10 | if isinstance(dobj, pdoc.External) and not external_links: 11 | return name 12 | url = dobj.url(relative_to=module, link_prefix=link_prefix, 13 | top_ancestor=not show_inherited_members) 14 | return '{}'.format(dobj.refname, url, name) 15 | 16 | 17 | def to_html(text): 18 | return _to_html(text, docformat=docformat, module=module, link=link, latex_math=latex_math) 19 | 20 | 21 | def get_annotation(bound_method, sep=':'): 22 | annot = show_type_annotations and bound_method(link=link) or '' 23 | if annot: 24 | annot = ' ' + sep + '\N{NBSP}' + annot 25 | return annot 26 | %> 27 | 28 | <%def name="ident(name)">${name} 29 | 30 | <%def name="show_source(d)"> 31 | % if (show_source_code or git_link_template) and d.source and d.obj is not getattr(d.inherits, 'obj', None): 32 | <% git_link = format_git_link(git_link_template, d) %> 33 | % if show_source_code: 34 |
35 | 36 | Expand source code 37 | % if git_link: 38 | Browse git 39 | %endif 40 | 41 |
${d.source | h}
42 |
43 | % elif git_link: 44 | 45 | %endif 46 | %endif 47 | 48 | 49 | <%def name="show_desc(d, short=False)"> 50 | <% 51 | inherits = ' inherited' if d.inherits else '' 52 | docstring = glimpse(d.docstring) if short or inherits else d.docstring 53 | %> 54 | % if d.inherits: 55 |

56 | Inherited from: 57 | % if hasattr(d.inherits, 'cls'): 58 | ${link(d.inherits.cls)}.${link(d.inherits, d.name)} 59 | % else: 60 | ${link(d.inherits)} 61 | % endif 62 |

63 | % endif 64 |
${docstring | to_html}
65 | % if not isinstance(d, pdoc.Module): 66 | ${show_source(d)} 67 | % endif 68 | 69 | 70 | <%def name="show_module_list(modules)"> 71 |

Python module list

72 | 73 | % if not modules: 74 |

No modules found.

75 | % else: 76 |
77 | % for name, desc in modules: 78 |
79 |
${name}
80 |
${desc | glimpse, to_html}
81 |
82 | % endfor 83 |
84 | % endif 85 | 86 | 87 | <%def name="show_column_list(items)"> 88 | <% 89 | two_column = len(items) >= 6 and all(len(i.name) < 20 for i in items) 90 | %> 91 |
    92 | % for item in items: 93 |
  • ${link(item, item.name)}
  • 94 | % endfor 95 |
96 | 97 | 98 | <%def name="show_module(module)"> 99 | <% 100 | variables = module.variables(sort=sort_identifiers) 101 | classes = module.classes(sort=sort_identifiers) 102 | functions = module.functions(sort=sort_identifiers) 103 | submodules = module.submodules() 104 | %> 105 | 106 | <%def name="show_func(f)"> 107 |
108 | <% 109 | params = ', '.join(f.params(annotate=show_type_annotations, link=link)) 110 | return_type = get_annotation(f.return_annotation, '\N{non-breaking hyphen}>') 111 | %> 112 | ${f.funcdef()} ${ident(f.name)}(${params})${return_type} 113 |
114 |
${show_desc(f)}
115 | 116 | 117 |
118 | % if http_server: 119 | 127 | % endif 128 |

${'Namespace' if module.is_namespace else \ 129 | 'Package' if module.is_package and not module.supermodule else \ 130 | 'Module'} ${module.name}

131 |
132 | 133 |
134 | ${module.docstring | to_html} 135 | ${show_source(module)} 136 |
137 | 138 |
139 | % if submodules: 140 |

Sub-modules

141 |
142 | % for m in submodules: 143 |
${link(m)}
144 |
${show_desc(m, short=True)}
145 | % endfor 146 |
147 | % endif 148 |
149 | 150 |
151 | % if variables: 152 |

Global variables

153 |
154 | % for v in variables: 155 | <% return_type = get_annotation(v.type_annotation) %> 156 |
var ${ident(v.name)}${return_type}
157 |
${show_desc(v)}
158 | % endfor 159 |
160 | % endif 161 |
162 | 163 |
164 | % if functions: 165 |

Functions

166 |
167 | % for f in functions: 168 | ${show_func(f)} 169 | % endfor 170 |
171 | % endif 172 |
173 | 174 |
175 | % if classes: 176 |

Classes

177 |
178 | % for c in classes: 179 | <% 180 | class_vars = c.class_variables(show_inherited_members, sort=sort_identifiers) 181 | smethods = c.functions(show_inherited_members, sort=sort_identifiers) 182 | inst_vars = c.instance_variables(show_inherited_members, sort=sort_identifiers) 183 | methods = c.methods(show_inherited_members, sort=sort_identifiers) 184 | mro = c.mro() 185 | subclasses = c.subclasses() 186 | params = ', '.join(c.params(annotate=show_type_annotations, link=link)) 187 | %> 188 |
189 | class ${ident(c.name)} 190 | % if params: 191 | (${params}) 192 | % endif 193 |
194 | 195 |
${show_desc(c)} 196 | 197 | % if mro: 198 |

Ancestors

199 |
    200 | % for cls in mro: 201 |
  • ${link(cls)}
  • 202 | % endfor 203 |
204 | %endif 205 | 206 | % if subclasses: 207 |

Subclasses

208 |
    209 | % for sub in subclasses: 210 |
  • ${link(sub)}
  • 211 | % endfor 212 |
213 | % endif 214 | % if class_vars: 215 |

Class variables

216 |
217 | % for v in class_vars: 218 | <% return_type = get_annotation(v.type_annotation) %> 219 |
var ${ident(v.name)}${return_type}
220 |
${show_desc(v)}
221 | % endfor 222 |
223 | % endif 224 | % if smethods: 225 |

Static methods

226 |
227 | % for f in smethods: 228 | ${show_func(f)} 229 | % endfor 230 |
231 | % endif 232 | % if inst_vars: 233 |

Instance variables

234 |
235 | % for v in inst_vars: 236 | <% return_type = get_annotation(v.type_annotation) %> 237 |
var ${ident(v.name)}${return_type}
238 |
${show_desc(v)}
239 | % endfor 240 |
241 | % endif 242 | % if methods: 243 |

Methods

244 |
245 | % for f in methods: 246 | ${show_func(f)} 247 | % endfor 248 |
249 | % endif 250 | 251 | % if not show_inherited_members: 252 | <% 253 | members = c.inherited_members() 254 | %> 255 | % if members: 256 |

Inherited members

257 |
    258 | % for cls, mems in members: 259 |
  • ${link(cls)}: 260 |
      261 | % for m in mems: 262 |
    • ${link(m, name=m.name)}
    • 263 | % endfor 264 |
    265 | 266 |
  • 267 | % endfor 268 |
269 | % endif 270 | % endif 271 | 272 |
273 | % endfor 274 |
275 | % endif 276 |
277 | 278 | 279 | <%def name="module_index(module)"> 280 | <% 281 | variables = module.variables(sort=sort_identifiers) 282 | classes = module.classes(sort=sort_identifiers) 283 | functions = module.functions(sort=sort_identifiers) 284 | submodules = module.submodules() 285 | supermodule = module.supermodule 286 | %> 287 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | <% 368 | module_list = 'modules' in context.keys() # Whether we're showing module list in server mode 369 | %> 370 | 371 | % if module_list: 372 | Python module list 373 | 374 | % else: 375 | ${module.name} API documentation 376 | 377 | % endif 378 | 379 | 380 | 381 | % if syntax_highlighting: 382 | 383 | %endif 384 | 385 | <%namespace name="css" file="css.mako" /> 386 | 387 | 388 | 389 | 390 | % if google_analytics: 391 | 395 | % endif 396 | 397 | % if search_query: 398 | 399 | 400 | 404 | % endif 405 | 406 | % if latex_math: 407 | 408 | % endif 409 | 410 | % if syntax_highlighting: 411 | 412 | 413 | % endif 414 | 415 | <%include file="head.mako"/> 416 | 417 | 418 |
419 | % if module_list: 420 |
421 | ${show_module_list(modules)} 422 |
423 | % else: 424 |
425 | ${show_module(module)} 426 |
427 | ${module_index(module)} 428 | % endif 429 |
430 | 431 | 435 | 436 | % if http_server and module: ## Auto-reload on file change in dev mode 437 | 445 | % endif 446 | 447 | 448 | -------------------------------------------------------------------------------- /docs/utils.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | logiclocking.utils API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |

Module logiclocking.utils

23 |
24 |
25 |

Various utils for working with logic-locked circuits.

26 |
27 | 28 | Expand source code 29 | 30 |
"""Various utils for working with logic-locked circuits."""
 31 | from ast import literal_eval
 32 | 
 33 | import circuitgraph as cg
 34 | 
 35 | 
 36 | def check_for_difference(oracle, locked_circuit, key):
 37 |     """
 38 |     Check the correctness of a key.
 39 | 
 40 |     Returns any differences between the oracle and a locked circuit
 41 |     with a specific key applied.
 42 | 
 43 |     Parameters
 44 |     ----------
 45 |     oracle: circuitgraph.Circuit
 46 |             The unlocked circuit to check against.
 47 |     locked_circuit: circuitgraph.Circuit
 48 |             The locked circuit to apply the key to.
 49 |     key: dict of str:bool
 50 |             The key to check
 51 | 
 52 |     Returns
 53 |     -------
 54 |     False or dict of str:bool
 55 |             False if there is no difference, otherwise the assignment that
 56 |             produced a difference.
 57 | 
 58 |     """
 59 |     m = cg.tx.miter(oracle, locked_circuit)
 60 |     key = {f"c1_{k}": v for k, v in key.items()}
 61 | 
 62 |     live = cg.sat.solve(m, assumptions=key)
 63 |     if not live:
 64 |         return True
 65 | 
 66 |     return cg.sat.solve(m, assumptions={"sat": True, **key})
 67 | 
 68 | 
 69 | def locked_unroll(
 70 |     locked_circuit,
 71 |     key,
 72 |     num_copies,
 73 |     D="D",
 74 |     Q="Q",
 75 |     ignore_pins="CK",
 76 |     initial_values=None,
 77 |     remove_unloaded=True,
 78 |     prefix="cg_unroll",
 79 | ):
 80 |     """
 81 |     Unrolls a sequential circuit to prepare for a combinational attack.
 82 | 
 83 |     This can be used for locks applied on sequential circuits that prevent
 84 |     scan-chain access.
 85 |     Note that this function uses the `prefix` variable to identify unrolled
 86 |     nodes, so choosing a prefix that is already used in nodes in the sequential
 87 |     circuit can cause undefined behavior.
 88 | 
 89 |     Parameters
 90 |     ----------
 91 |     locked_circuit: circuitgraph.Circuit
 92 |             The circuit to unroll
 93 |     key: list of str or dict of str:bool
 94 |             The key inputs. If a dictionary is passed in, this key is used
 95 |             to construct an unrolled oracle and this is returned in addition
 96 |             to the unrolled locked circuit
 97 |     num_copies: int
 98 |             The number of unrolled copies of the circuit to create
 99 |     D: str
100 |             The name of the D pin of the sequential elements
101 |     Q: str
102 |             The name of the Q pin of the sequential elements
103 |     ignore_pins: str or list of str
104 |             The pins on the sequential elements to ignore
105 |     initial_values: str or dict of str:str
106 |             The initial values of the data ports for the first timestep.
107 |             If None, the ports will be added as primary inputs.
108 |             If a single value ('0', '1', or 'x'), every flop will get that value.
109 |             Can also pass in dict mapping flop names to values.
110 |     remove_unloaded: bool
111 |             If True, unloaded inputs will be removed after unrolling. This can remove
112 |             unused sequential signals such as the clock and reset.
113 |     prefix: str
114 |             The prefix to use for naming unrolled nodes.
115 | 
116 |     Returns
117 |     -------
118 |     circuitgraph.Circuit, circuitgraph.Circuit, dict of str:list of str
119 |             The unrolled locked circuit, the unrolled oracle, and the io map.
120 | 
121 |     """
122 |     locked_circuit_unrolled, io_map = cg.tx.sequential_unroll(
123 |         locked_circuit,
124 |         num_copies,
125 |         D,
126 |         Q,
127 |         ignore_pins=ignore_pins,
128 |         initial_values=initial_values,
129 |         remove_unloaded=remove_unloaded,
130 |         prefix=prefix,
131 |     )
132 | 
133 |     for k in key:
134 |         locked_circuit_unrolled.set_type(io_map[k], "buf")
135 |         locked_circuit_unrolled.add(k, "input", fanout=io_map[k])
136 |         del io_map[k]
137 | 
138 |     oracle_unrolled = locked_circuit_unrolled.copy()
139 |     for k, v in key.items():
140 |         oracle_unrolled.set_type(k, str(int(v)))
141 | 
142 |     return locked_circuit_unrolled, oracle_unrolled, io_map
143 | 
144 | 
145 | def write_key(key, filename):
146 |     """Write a key dictionary to a file."""
147 |     with open(filename, "w") as f:
148 |         f.write(str(key) + "\n")
149 | 
150 | 
151 | def read_key(filename):
152 |     """Read a key dictionary from a file."""
153 |     with open(filename) as f:
154 |         return literal_eval(f.read())
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |

Functions

163 |
164 |
165 | def check_for_difference(oracle, locked_circuit, key) 166 |
167 |
168 |

Check the correctness of a key.

169 |

Returns any differences between the oracle and a locked circuit 170 | with a specific key applied.

171 |

Parameters

172 |
173 |
oracle : circuitgraph.Circuit
174 |
The unlocked circuit to check against.
175 |
locked_circuit : circuitgraph.Circuit
176 |
The locked circuit to apply the key to.
177 |
key : dict of str:bool
178 |
The key to check
179 |
180 |

Returns

181 |
182 |
False or dict of str:bool
183 |
False if there is no difference, otherwise the assignment that 184 | produced a difference.
185 |
186 |
187 | 188 | Expand source code 189 | 190 |
def check_for_difference(oracle, locked_circuit, key):
191 |     """
192 |     Check the correctness of a key.
193 | 
194 |     Returns any differences between the oracle and a locked circuit
195 |     with a specific key applied.
196 | 
197 |     Parameters
198 |     ----------
199 |     oracle: circuitgraph.Circuit
200 |             The unlocked circuit to check against.
201 |     locked_circuit: circuitgraph.Circuit
202 |             The locked circuit to apply the key to.
203 |     key: dict of str:bool
204 |             The key to check
205 | 
206 |     Returns
207 |     -------
208 |     False or dict of str:bool
209 |             False if there is no difference, otherwise the assignment that
210 |             produced a difference.
211 | 
212 |     """
213 |     m = cg.tx.miter(oracle, locked_circuit)
214 |     key = {f"c1_{k}": v for k, v in key.items()}
215 | 
216 |     live = cg.sat.solve(m, assumptions=key)
217 |     if not live:
218 |         return True
219 | 
220 |     return cg.sat.solve(m, assumptions={"sat": True, **key})
221 |
222 |
223 |
224 | def locked_unroll(locked_circuit, key, num_copies, D='D', Q='Q', ignore_pins='CK', initial_values=None, remove_unloaded=True, prefix='cg_unroll') 225 |
226 |
227 |

Unrolls a sequential circuit to prepare for a combinational attack.

228 |

This can be used for locks applied on sequential circuits that prevent 229 | scan-chain access. 230 | Note that this function uses the prefix variable to identify unrolled 231 | nodes, so choosing a prefix that is already used in nodes in the sequential 232 | circuit can cause undefined behavior.

233 |

Parameters

234 |
235 |
locked_circuit : circuitgraph.Circuit
236 |
The circuit to unroll
237 |
key : list of str or dict of str:bool
238 |
The key inputs. If a dictionary is passed in, this key is used 239 | to construct an unrolled oracle and this is returned in addition 240 | to the unrolled locked circuit
241 |
num_copies : int
242 |
The number of unrolled copies of the circuit to create
243 |
D : str
244 |
The name of the D pin of the sequential elements
245 |
Q : str
246 |
The name of the Q pin of the sequential elements
247 |
ignore_pins : str or list of str
248 |
The pins on the sequential elements to ignore
249 |
initial_values : str or dict of str:str
250 |
The initial values of the data ports for the first timestep. 251 | If None, the ports will be added as primary inputs. 252 | If a single value ('0', '1', or 'x'), every flop will get that value. 253 | Can also pass in dict mapping flop names to values.
254 |
remove_unloaded : bool
255 |
If True, unloaded inputs will be removed after unrolling. This can remove 256 | unused sequential signals such as the clock and reset.
257 |
prefix : str
258 |
The prefix to use for naming unrolled nodes.
259 |
260 |

Returns

261 |
262 |
circuitgraph.Circuit, circuitgraph.Circuit, dict of str:list of str
263 |
The unrolled locked circuit, the unrolled oracle, and the io map.
264 |
265 |
266 | 267 | Expand source code 268 | 269 |
def locked_unroll(
270 |     locked_circuit,
271 |     key,
272 |     num_copies,
273 |     D="D",
274 |     Q="Q",
275 |     ignore_pins="CK",
276 |     initial_values=None,
277 |     remove_unloaded=True,
278 |     prefix="cg_unroll",
279 | ):
280 |     """
281 |     Unrolls a sequential circuit to prepare for a combinational attack.
282 | 
283 |     This can be used for locks applied on sequential circuits that prevent
284 |     scan-chain access.
285 |     Note that this function uses the `prefix` variable to identify unrolled
286 |     nodes, so choosing a prefix that is already used in nodes in the sequential
287 |     circuit can cause undefined behavior.
288 | 
289 |     Parameters
290 |     ----------
291 |     locked_circuit: circuitgraph.Circuit
292 |             The circuit to unroll
293 |     key: list of str or dict of str:bool
294 |             The key inputs. If a dictionary is passed in, this key is used
295 |             to construct an unrolled oracle and this is returned in addition
296 |             to the unrolled locked circuit
297 |     num_copies: int
298 |             The number of unrolled copies of the circuit to create
299 |     D: str
300 |             The name of the D pin of the sequential elements
301 |     Q: str
302 |             The name of the Q pin of the sequential elements
303 |     ignore_pins: str or list of str
304 |             The pins on the sequential elements to ignore
305 |     initial_values: str or dict of str:str
306 |             The initial values of the data ports for the first timestep.
307 |             If None, the ports will be added as primary inputs.
308 |             If a single value ('0', '1', or 'x'), every flop will get that value.
309 |             Can also pass in dict mapping flop names to values.
310 |     remove_unloaded: bool
311 |             If True, unloaded inputs will be removed after unrolling. This can remove
312 |             unused sequential signals such as the clock and reset.
313 |     prefix: str
314 |             The prefix to use for naming unrolled nodes.
315 | 
316 |     Returns
317 |     -------
318 |     circuitgraph.Circuit, circuitgraph.Circuit, dict of str:list of str
319 |             The unrolled locked circuit, the unrolled oracle, and the io map.
320 | 
321 |     """
322 |     locked_circuit_unrolled, io_map = cg.tx.sequential_unroll(
323 |         locked_circuit,
324 |         num_copies,
325 |         D,
326 |         Q,
327 |         ignore_pins=ignore_pins,
328 |         initial_values=initial_values,
329 |         remove_unloaded=remove_unloaded,
330 |         prefix=prefix,
331 |     )
332 | 
333 |     for k in key:
334 |         locked_circuit_unrolled.set_type(io_map[k], "buf")
335 |         locked_circuit_unrolled.add(k, "input", fanout=io_map[k])
336 |         del io_map[k]
337 | 
338 |     oracle_unrolled = locked_circuit_unrolled.copy()
339 |     for k, v in key.items():
340 |         oracle_unrolled.set_type(k, str(int(v)))
341 | 
342 |     return locked_circuit_unrolled, oracle_unrolled, io_map
343 |
344 |
345 |
346 | def read_key(filename) 347 |
348 |
349 |

Read a key dictionary from a file.

350 |
351 | 352 | Expand source code 353 | 354 |
def read_key(filename):
355 |     """Read a key dictionary from a file."""
356 |     with open(filename) as f:
357 |         return literal_eval(f.read())
358 |
359 |
360 |
361 | def write_key(key, filename) 362 |
363 |
364 |

Write a key dictionary to a file.

365 |
366 | 367 | Expand source code 368 | 369 |
def write_key(key, filename):
370 |     """Write a key dictionary to a file."""
371 |     with open(filename, "w") as f:
372 |         f.write(str(key) + "\n")
373 |
374 |
375 |
376 |
377 |
378 |
379 |
380 | 405 |
406 | 409 | 410 | -------------------------------------------------------------------------------- /docs/attacks.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | logiclocking.attacks API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |

Module logiclocking.attacks

23 |
24 |
25 |

Run attacks on logic-locked circuits.

26 |
27 | 28 | Expand source code 29 | 30 |
"""Run attacks on logic-locked circuits."""
 31 | import code
 32 | import random
 33 | import time
 34 | 
 35 | import circuitgraph as cg
 36 | 
 37 | 
 38 | def _localtime():
 39 |     return time.asctime(time.localtime(time.time()))
 40 | 
 41 | 
 42 | def miter_attack(
 43 |     cl,
 44 |     key,
 45 |     timeout=None,
 46 |     key_cons=None,
 47 |     unroll_cyclic=True,
 48 |     verbose=True,
 49 |     code_on_error=False,
 50 | ):
 51 |     """
 52 |     Launch a miter-based sat attack on a locked circuit.
 53 | 
 54 |     Parameters
 55 |     ----------
 56 |     cl: circuitgraph.Circuit
 57 |             The locked circuit to attack
 58 |     key: dict of str:bool
 59 |             The correct key, used to construct the oracle
 60 |     timeout: int
 61 |             Timeout for the attack, in seconds
 62 |     key_cons: circuitgraph.Circuit or iter of circuitgraph.Circuit
 63 |             Key conditions to satisfy during attack,
 64 |             must have output 'sat' and be a function of the key inputs
 65 |     unroll_cyclic: bool
 66 |             If True, convert cyclic circuits to acyclic versions
 67 |     verbose: bool
 68 |             If True, attack progress will be printed
 69 |     code_on_error: bool
 70 |             If True, drop into an interactive session on an error
 71 | 
 72 |     Returns
 73 |     -------
 74 |     dict
 75 |             A dictionary containing attack info and results
 76 | 
 77 |     """
 78 |     start_time = time.time()
 79 | 
 80 |     if cl.is_cyclic():
 81 |         if unroll_cyclic:
 82 |             cl = cg.tx.acyclic_unroll(cl)
 83 |         else:
 84 |             raise ValueError(
 85 |                 "Circuit is cyclic. Set 'unroll_cyclic' to True to run sat on "
 86 |                 "this circuit"
 87 |             )
 88 | 
 89 |     # setup vars
 90 |     keys = tuple(key.keys())
 91 |     ins = tuple(cl.startpoints() - key.keys())
 92 |     outs = tuple(cl.endpoints())
 93 | 
 94 |     # create simulation solver
 95 |     s_sim, v_sim = cg.sat.construct_solver(cl, key)
 96 | 
 97 |     # create miter solver
 98 |     m = cg.tx.miter(cl, startpoints=set(ins))
 99 |     s_miter, v_miter = cg.sat.construct_solver(m)
100 | 
101 |     # add key constraints
102 |     if key_cons:
103 |         if isinstance(key_cons, cg.Circuit):
104 |             key_cons = [key_cons]
105 |         for key_con in key_cons:
106 |             if verbose:
107 |                 print(
108 |                     f"[{_localtime()}] circuit: {cl.name}, "
109 |                     f"adding constraints: {key_con.name}"
110 |                 )
111 |             formula, v_cons = cg.sat.cnf(key_con)
112 |             con_clauses = formula.clauses
113 | 
114 |             # add constraints circuits
115 |             c0_offset = s_miter.nof_vars()
116 |             c0 = cg.sat.remap(con_clauses, c0_offset)
117 |             s_miter.append_formula(c0)
118 |             c1_offset = s_miter.nof_vars()
119 |             c1 = cg.sat.remap(con_clauses, c1_offset)
120 |             s_miter.append_formula(c1)
121 | 
122 |             # encode keys connections
123 |             clauses = [[v_cons.id("sat") + c0_offset], [v_cons.id("sat") + c1_offset]]
124 |             clauses += [
125 |                 [-v_miter.id(f"c0_{n}"), v_cons.id(n) + c0_offset] for n in keys
126 |             ]
127 |             clauses += [
128 |                 [v_miter.id(f"c0_{n}"), -v_cons.id(n) - c0_offset] for n in keys
129 |             ]
130 |             clauses += [
131 |                 [-v_miter.id(f"c1_{n}"), v_cons.id(n) + c1_offset] for n in keys
132 |             ]
133 |             clauses += [
134 |                 [v_miter.id(f"c1_{n}"), -v_cons.id(n) - c1_offset] for n in keys
135 |             ]
136 | 
137 |             s_miter.append_formula(clauses)
138 | 
139 |     # get circuit clauses
140 |     formula, v_c = cg.sat.cnf(cl)
141 |     clauses = formula.clauses
142 | 
143 |     # solve
144 |     dis = []
145 |     dos = []
146 |     iter_times = []
147 |     iter_keys = []
148 |     while s_miter.solve(assumptions=[v_miter.id("sat")]):
149 | 
150 |         # get di
151 |         model = s_miter.get_model()
152 |         di = [model[v_miter.id(n) - 1] > 0 for n in ins]
153 |         if tuple(di) in dis:
154 |             if code_on_error:
155 |                 print("Error di")
156 |                 code.interact(local=dict(globals(), **locals()))
157 |             else:
158 |                 raise ValueError("Saw same di twice")
159 | 
160 |         # get intermediate keys
161 |         k0 = {n: model[v_miter.id(f"c0_{n}") - 1] > 0 for n in keys}
162 |         k1 = {n: model[v_miter.id(f"c1_{n}") - 1] > 0 for n in keys}
163 |         iter_keys.append((k0, k1))
164 | 
165 |         # get do
166 |         s_sim.solve(assumptions=[(2 * b - 1) * v_sim.id(n) for b, n in zip(di, ins)])
167 |         model = s_sim.get_model()
168 |         if model is None:
169 |             if code_on_error:
170 |                 print("Error sim")
171 |                 code.interact(local=dict(globals(), **locals()))
172 |             else:
173 |                 raise ValueError("Could not get simulation model")
174 |         do = [model[v_sim.id(n) - 1] > 0 for n in outs]
175 |         dis.append(tuple(di))
176 |         dos.append(tuple(do))
177 |         iter_times.append(time.time() - start_time)
178 | 
179 |         # add constraints circuits
180 |         c0_offset = s_miter.nof_vars()
181 |         c0 = cg.sat.remap(clauses, c0_offset)
182 |         s_miter.append_formula(c0)
183 |         c1_offset = s_miter.nof_vars()
184 |         c1 = cg.sat.remap(clauses, c1_offset)
185 |         s_miter.append_formula(c1)
186 | 
187 |         # encode dis + dos
188 |         dio_clauses = [
189 |             [(2 * b - 1) * (v_c.id(n) + c0_offset)] for b, n in zip(di + do, ins + outs)
190 |         ]
191 |         dio_clauses += [
192 |             [(2 * b - 1) * (v_c.id(n) + c1_offset)] for b, n in zip(di + do, ins + outs)
193 |         ]
194 |         s_miter.append_formula(dio_clauses)
195 | 
196 |         # encode keys connections
197 |         key_clauses = [[-v_miter.id(f"c0_{n}"), v_c.id(n) + c0_offset] for n in keys]
198 |         key_clauses += [[v_miter.id(f"c0_{n}"), -v_c.id(n) - c0_offset] for n in keys]
199 |         key_clauses += [[-v_miter.id(f"c1_{n}"), v_c.id(n) + c1_offset] for n in keys]
200 |         key_clauses += [[v_miter.id(f"c1_{n}"), -v_c.id(n) - c1_offset] for n in keys]
201 |         s_miter.append_formula(key_clauses)
202 | 
203 |         # check timeout
204 |         if timeout and (time.time() - start_time) > timeout:
205 |             print(f"[{_localtime()}] circuit: {cl.name}, Timeout: True")
206 |             return {
207 |                 "Time": None,
208 |                 "Iterations": len(dis),
209 |                 "Timeout": True,
210 |                 "Equivalent": False,
211 |                 "Key Found": False,
212 |                 "dis": dis,
213 |                 "dos": dos,
214 |                 "iter_times": iter_times,
215 |                 "iter_keys": iter_keys,
216 |             }
217 | 
218 |         if verbose:
219 |             print(
220 |                 f"[{_localtime()}] "
221 |                 f"circuit: {cl.name}, iter: {len(dis)}, "
222 |                 f"time: {time.time()-start_time}, "
223 |                 f"clauses: {s_miter.nof_clauses()}, "
224 |                 f"vars: {s_miter.nof_vars()}"
225 |             )
226 | 
227 |     # check if a satisfying key remains
228 |     key_found = s_miter.solve()
229 |     if verbose:
230 |         print(f"[{_localtime()}] circuit: {cl.name}, key found: {key_found}")
231 |     if not key_found:
232 |         return {
233 |             "Time": None,
234 |             "Iterations": len(dis),
235 |             "Timeout": False,
236 |             "Equivalent": False,
237 |             "Key Found": False,
238 |             "dis": dis,
239 |             "dos": dos,
240 |             "iter_times": iter_times,
241 |             "iter_keys": iter_keys,
242 |         }
243 | 
244 |     # get key
245 |     model = s_miter.get_model()
246 |     attack_key = {n: model[v_miter.id(f"c1_{n}") - 1] > 0 for n in keys}
247 | 
248 |     # check key
249 |     assumptions = {
250 |         **{f"c0_{k}": v for k, v in key.items()},
251 |         **{f"c1_{k}": v for k, v in attack_key.items()},
252 |         "sat": True,
253 |     }
254 |     equivalent = not cg.sat.solve(m, assumptions)
255 |     if verbose:
256 |         print(f"[{_localtime()}] circuit: {cl.name}, equivalent: {equivalent}")
257 | 
258 |     exec_time = time.time() - start_time
259 |     if verbose:
260 |         print(f"[{_localtime()}] circuit: {cl.name}, elasped time: {exec_time}")
261 | 
262 |     return {
263 |         "Time": exec_time,
264 |         "Iterations": len(dis),
265 |         "Timeout": False,
266 |         "Equivalent": equivalent,
267 |         "Key Found": True,
268 |         "dis": dis,
269 |         "dos": dos,
270 |         "iter_times": iter_times,
271 |         "iter_keys": iter_keys,
272 |         "attack_key": attack_key,
273 |     }
274 | 
275 | 
276 | def decision_tree_attack(c_or_cl, nsamples, key=None, verbose=True):
277 |     """
278 |     Launch a decision tree attack on a locked circuit.
279 | 
280 |     Attempts to capture the functionality of the oracle circuit using a
281 |     decision tree.
282 | 
283 |     Paramters
284 |     ---------
285 |     c_or_cl: circuitgraph.Circuit
286 |             The circuit to reverse engineer. Can either be
287 |             the oracle or the locked circuit. If the locked
288 |             circuit, must pass in the correct key using
289 |             the `key` parameter
290 |     nsamples: int
291 |             The number of samples to train the decision tree on
292 |     key: dict of str:bool
293 |             The correct key, used to construct the oracle if
294 |             the locked circuit is given.
295 |     verbose: bool
296 |             If True, attack progress will be printed
297 | 
298 |     Returns
299 |     -------
300 |     dict of str:sklearn.tree.DecisionTreeClassifier
301 |             The trained classifier for each output.
302 | 
303 |     """
304 |     from sklearn.tree import DecisionTreeClassifier
305 | 
306 |     if key:
307 |         cl = c_or_cl
308 |         for k, v in key.items():
309 |             cl.set_type(k, str(int(v)))
310 |         c = cl
311 |     else:
312 |         c = c_or_cl
313 | 
314 |     ins = tuple(c.startpoints())
315 |     outs = tuple(c.endpoints())
316 | 
317 |     # generate training samples
318 |     x = []
319 |     y = {o: [] for o in outs}
320 |     if verbose:
321 |         print(f"[{_localtime()}] Generating samples")
322 |     for i in range(nsamples):
323 |         x += [[random.choice((True, False)) for i in ins]]
324 |         result = cg.sat.solve(c, {i: v for i, v in zip(ins, x[-1])})
325 |         for o in outs:
326 |             y[o] += [result[o]]
327 | 
328 |     if verbose:
329 |         print(f"[{_localtime()}] Training decision trees")
330 |     estimators = {o: DecisionTreeClassifier() for o in outs}
331 |     for o in outs:
332 |         estimators[o].fit(x, y[o])
333 | 
334 |     if verbose:
335 |         print(f"[{_localtime()}] Testing decision trees")
336 |     ncorrect = 0
337 |     for i in range(nsamples):
338 |         x = [[random.choice((True, False)) for i in ins]]
339 |         result = cg.sat.solve(c, {i: v for i, v in zip(ins, x[-1])})
340 |         if all(result[o] == estimators[o].predict(x) for o in outs):
341 |             ncorrect += 1
342 | 
343 |     if verbose:
344 |         print(f"[{_localtime()}] Test accuracy: {ncorrect / nsamples}")
345 |     return estimators
346 |
347 |
348 |
349 |
350 |
351 |
352 |
353 |

Functions

354 |
355 |
356 | def decision_tree_attack(c_or_cl, nsamples, key=None, verbose=True) 357 |
358 |
359 |

Launch a decision tree attack on a locked circuit.

360 |

Attempts to capture the functionality of the oracle circuit using a 361 | decision tree.

362 |

Paramters

363 |

c_or_cl: circuitgraph.Circuit 364 | The circuit to reverse engineer. Can either be 365 | the oracle or the locked circuit. If the locked 366 | circuit, must pass in the correct key using 367 | the key parameter 368 | nsamples: int 369 | The number of samples to train the decision tree on 370 | key: dict of str:bool 371 | The correct key, used to construct the oracle if 372 | the locked circuit is given. 373 | verbose: bool 374 | If True, attack progress will be printed

375 |

Returns

376 |
377 |
dict of str:sklearn.tree.DecisionTreeClassifier
378 |
The trained classifier for each output.
379 |
380 |
381 | 382 | Expand source code 383 | 384 |
def decision_tree_attack(c_or_cl, nsamples, key=None, verbose=True):
385 |     """
386 |     Launch a decision tree attack on a locked circuit.
387 | 
388 |     Attempts to capture the functionality of the oracle circuit using a
389 |     decision tree.
390 | 
391 |     Paramters
392 |     ---------
393 |     c_or_cl: circuitgraph.Circuit
394 |             The circuit to reverse engineer. Can either be
395 |             the oracle or the locked circuit. If the locked
396 |             circuit, must pass in the correct key using
397 |             the `key` parameter
398 |     nsamples: int
399 |             The number of samples to train the decision tree on
400 |     key: dict of str:bool
401 |             The correct key, used to construct the oracle if
402 |             the locked circuit is given.
403 |     verbose: bool
404 |             If True, attack progress will be printed
405 | 
406 |     Returns
407 |     -------
408 |     dict of str:sklearn.tree.DecisionTreeClassifier
409 |             The trained classifier for each output.
410 | 
411 |     """
412 |     from sklearn.tree import DecisionTreeClassifier
413 | 
414 |     if key:
415 |         cl = c_or_cl
416 |         for k, v in key.items():
417 |             cl.set_type(k, str(int(v)))
418 |         c = cl
419 |     else:
420 |         c = c_or_cl
421 | 
422 |     ins = tuple(c.startpoints())
423 |     outs = tuple(c.endpoints())
424 | 
425 |     # generate training samples
426 |     x = []
427 |     y = {o: [] for o in outs}
428 |     if verbose:
429 |         print(f"[{_localtime()}] Generating samples")
430 |     for i in range(nsamples):
431 |         x += [[random.choice((True, False)) for i in ins]]
432 |         result = cg.sat.solve(c, {i: v for i, v in zip(ins, x[-1])})
433 |         for o in outs:
434 |             y[o] += [result[o]]
435 | 
436 |     if verbose:
437 |         print(f"[{_localtime()}] Training decision trees")
438 |     estimators = {o: DecisionTreeClassifier() for o in outs}
439 |     for o in outs:
440 |         estimators[o].fit(x, y[o])
441 | 
442 |     if verbose:
443 |         print(f"[{_localtime()}] Testing decision trees")
444 |     ncorrect = 0
445 |     for i in range(nsamples):
446 |         x = [[random.choice((True, False)) for i in ins]]
447 |         result = cg.sat.solve(c, {i: v for i, v in zip(ins, x[-1])})
448 |         if all(result[o] == estimators[o].predict(x) for o in outs):
449 |             ncorrect += 1
450 | 
451 |     if verbose:
452 |         print(f"[{_localtime()}] Test accuracy: {ncorrect / nsamples}")
453 |     return estimators
454 |
455 |
456 |
457 | def miter_attack(cl, key, timeout=None, key_cons=None, unroll_cyclic=True, verbose=True, code_on_error=False) 458 |
459 |
460 |

Launch a miter-based sat attack on a locked circuit.

461 |

Parameters

462 |
463 |
cl : circuitgraph.Circuit
464 |
The locked circuit to attack
465 |
key : dict of str:bool
466 |
The correct key, used to construct the oracle
467 |
timeout : int
468 |
Timeout for the attack, in seconds
469 |
key_cons : circuitgraph.Circuit or iter of circuitgraph.Circuit
470 |
Key conditions to satisfy during attack, 471 | must have output 'sat' and be a function of the key inputs
472 |
unroll_cyclic : bool
473 |
If True, convert cyclic circuits to acyclic versions
474 |
verbose : bool
475 |
If True, attack progress will be printed
476 |
code_on_error : bool
477 |
If True, drop into an interactive session on an error
478 |
479 |

Returns

480 |
481 |
dict
482 |
A dictionary containing attack info and results
483 |
484 |
485 | 486 | Expand source code 487 | 488 |
def miter_attack(
489 |     cl,
490 |     key,
491 |     timeout=None,
492 |     key_cons=None,
493 |     unroll_cyclic=True,
494 |     verbose=True,
495 |     code_on_error=False,
496 | ):
497 |     """
498 |     Launch a miter-based sat attack on a locked circuit.
499 | 
500 |     Parameters
501 |     ----------
502 |     cl: circuitgraph.Circuit
503 |             The locked circuit to attack
504 |     key: dict of str:bool
505 |             The correct key, used to construct the oracle
506 |     timeout: int
507 |             Timeout for the attack, in seconds
508 |     key_cons: circuitgraph.Circuit or iter of circuitgraph.Circuit
509 |             Key conditions to satisfy during attack,
510 |             must have output 'sat' and be a function of the key inputs
511 |     unroll_cyclic: bool
512 |             If True, convert cyclic circuits to acyclic versions
513 |     verbose: bool
514 |             If True, attack progress will be printed
515 |     code_on_error: bool
516 |             If True, drop into an interactive session on an error
517 | 
518 |     Returns
519 |     -------
520 |     dict
521 |             A dictionary containing attack info and results
522 | 
523 |     """
524 |     start_time = time.time()
525 | 
526 |     if cl.is_cyclic():
527 |         if unroll_cyclic:
528 |             cl = cg.tx.acyclic_unroll(cl)
529 |         else:
530 |             raise ValueError(
531 |                 "Circuit is cyclic. Set 'unroll_cyclic' to True to run sat on "
532 |                 "this circuit"
533 |             )
534 | 
535 |     # setup vars
536 |     keys = tuple(key.keys())
537 |     ins = tuple(cl.startpoints() - key.keys())
538 |     outs = tuple(cl.endpoints())
539 | 
540 |     # create simulation solver
541 |     s_sim, v_sim = cg.sat.construct_solver(cl, key)
542 | 
543 |     # create miter solver
544 |     m = cg.tx.miter(cl, startpoints=set(ins))
545 |     s_miter, v_miter = cg.sat.construct_solver(m)
546 | 
547 |     # add key constraints
548 |     if key_cons:
549 |         if isinstance(key_cons, cg.Circuit):
550 |             key_cons = [key_cons]
551 |         for key_con in key_cons:
552 |             if verbose:
553 |                 print(
554 |                     f"[{_localtime()}] circuit: {cl.name}, "
555 |                     f"adding constraints: {key_con.name}"
556 |                 )
557 |             formula, v_cons = cg.sat.cnf(key_con)
558 |             con_clauses = formula.clauses
559 | 
560 |             # add constraints circuits
561 |             c0_offset = s_miter.nof_vars()
562 |             c0 = cg.sat.remap(con_clauses, c0_offset)
563 |             s_miter.append_formula(c0)
564 |             c1_offset = s_miter.nof_vars()
565 |             c1 = cg.sat.remap(con_clauses, c1_offset)
566 |             s_miter.append_formula(c1)
567 | 
568 |             # encode keys connections
569 |             clauses = [[v_cons.id("sat") + c0_offset], [v_cons.id("sat") + c1_offset]]
570 |             clauses += [
571 |                 [-v_miter.id(f"c0_{n}"), v_cons.id(n) + c0_offset] for n in keys
572 |             ]
573 |             clauses += [
574 |                 [v_miter.id(f"c0_{n}"), -v_cons.id(n) - c0_offset] for n in keys
575 |             ]
576 |             clauses += [
577 |                 [-v_miter.id(f"c1_{n}"), v_cons.id(n) + c1_offset] for n in keys
578 |             ]
579 |             clauses += [
580 |                 [v_miter.id(f"c1_{n}"), -v_cons.id(n) - c1_offset] for n in keys
581 |             ]
582 | 
583 |             s_miter.append_formula(clauses)
584 | 
585 |     # get circuit clauses
586 |     formula, v_c = cg.sat.cnf(cl)
587 |     clauses = formula.clauses
588 | 
589 |     # solve
590 |     dis = []
591 |     dos = []
592 |     iter_times = []
593 |     iter_keys = []
594 |     while s_miter.solve(assumptions=[v_miter.id("sat")]):
595 | 
596 |         # get di
597 |         model = s_miter.get_model()
598 |         di = [model[v_miter.id(n) - 1] > 0 for n in ins]
599 |         if tuple(di) in dis:
600 |             if code_on_error:
601 |                 print("Error di")
602 |                 code.interact(local=dict(globals(), **locals()))
603 |             else:
604 |                 raise ValueError("Saw same di twice")
605 | 
606 |         # get intermediate keys
607 |         k0 = {n: model[v_miter.id(f"c0_{n}") - 1] > 0 for n in keys}
608 |         k1 = {n: model[v_miter.id(f"c1_{n}") - 1] > 0 for n in keys}
609 |         iter_keys.append((k0, k1))
610 | 
611 |         # get do
612 |         s_sim.solve(assumptions=[(2 * b - 1) * v_sim.id(n) for b, n in zip(di, ins)])
613 |         model = s_sim.get_model()
614 |         if model is None:
615 |             if code_on_error:
616 |                 print("Error sim")
617 |                 code.interact(local=dict(globals(), **locals()))
618 |             else:
619 |                 raise ValueError("Could not get simulation model")
620 |         do = [model[v_sim.id(n) - 1] > 0 for n in outs]
621 |         dis.append(tuple(di))
622 |         dos.append(tuple(do))
623 |         iter_times.append(time.time() - start_time)
624 | 
625 |         # add constraints circuits
626 |         c0_offset = s_miter.nof_vars()
627 |         c0 = cg.sat.remap(clauses, c0_offset)
628 |         s_miter.append_formula(c0)
629 |         c1_offset = s_miter.nof_vars()
630 |         c1 = cg.sat.remap(clauses, c1_offset)
631 |         s_miter.append_formula(c1)
632 | 
633 |         # encode dis + dos
634 |         dio_clauses = [
635 |             [(2 * b - 1) * (v_c.id(n) + c0_offset)] for b, n in zip(di + do, ins + outs)
636 |         ]
637 |         dio_clauses += [
638 |             [(2 * b - 1) * (v_c.id(n) + c1_offset)] for b, n in zip(di + do, ins + outs)
639 |         ]
640 |         s_miter.append_formula(dio_clauses)
641 | 
642 |         # encode keys connections
643 |         key_clauses = [[-v_miter.id(f"c0_{n}"), v_c.id(n) + c0_offset] for n in keys]
644 |         key_clauses += [[v_miter.id(f"c0_{n}"), -v_c.id(n) - c0_offset] for n in keys]
645 |         key_clauses += [[-v_miter.id(f"c1_{n}"), v_c.id(n) + c1_offset] for n in keys]
646 |         key_clauses += [[v_miter.id(f"c1_{n}"), -v_c.id(n) - c1_offset] for n in keys]
647 |         s_miter.append_formula(key_clauses)
648 | 
649 |         # check timeout
650 |         if timeout and (time.time() - start_time) > timeout:
651 |             print(f"[{_localtime()}] circuit: {cl.name}, Timeout: True")
652 |             return {
653 |                 "Time": None,
654 |                 "Iterations": len(dis),
655 |                 "Timeout": True,
656 |                 "Equivalent": False,
657 |                 "Key Found": False,
658 |                 "dis": dis,
659 |                 "dos": dos,
660 |                 "iter_times": iter_times,
661 |                 "iter_keys": iter_keys,
662 |             }
663 | 
664 |         if verbose:
665 |             print(
666 |                 f"[{_localtime()}] "
667 |                 f"circuit: {cl.name}, iter: {len(dis)}, "
668 |                 f"time: {time.time()-start_time}, "
669 |                 f"clauses: {s_miter.nof_clauses()}, "
670 |                 f"vars: {s_miter.nof_vars()}"
671 |             )
672 | 
673 |     # check if a satisfying key remains
674 |     key_found = s_miter.solve()
675 |     if verbose:
676 |         print(f"[{_localtime()}] circuit: {cl.name}, key found: {key_found}")
677 |     if not key_found:
678 |         return {
679 |             "Time": None,
680 |             "Iterations": len(dis),
681 |             "Timeout": False,
682 |             "Equivalent": False,
683 |             "Key Found": False,
684 |             "dis": dis,
685 |             "dos": dos,
686 |             "iter_times": iter_times,
687 |             "iter_keys": iter_keys,
688 |         }
689 | 
690 |     # get key
691 |     model = s_miter.get_model()
692 |     attack_key = {n: model[v_miter.id(f"c1_{n}") - 1] > 0 for n in keys}
693 | 
694 |     # check key
695 |     assumptions = {
696 |         **{f"c0_{k}": v for k, v in key.items()},
697 |         **{f"c1_{k}": v for k, v in attack_key.items()},
698 |         "sat": True,
699 |     }
700 |     equivalent = not cg.sat.solve(m, assumptions)
701 |     if verbose:
702 |         print(f"[{_localtime()}] circuit: {cl.name}, equivalent: {equivalent}")
703 | 
704 |     exec_time = time.time() - start_time
705 |     if verbose:
706 |         print(f"[{_localtime()}] circuit: {cl.name}, elasped time: {exec_time}")
707 | 
708 |     return {
709 |         "Time": exec_time,
710 |         "Iterations": len(dis),
711 |         "Timeout": False,
712 |         "Equivalent": equivalent,
713 |         "Key Found": True,
714 |         "dis": dis,
715 |         "dos": dos,
716 |         "iter_times": iter_times,
717 |         "iter_keys": iter_keys,
718 |         "attack_key": attack_key,
719 |     }
720 |
721 |
722 |
723 |
724 |
725 |
726 |
727 | 750 |
751 | 754 | 755 | -------------------------------------------------------------------------------- /logiclocking/locks.py: -------------------------------------------------------------------------------- 1 | """Apply logic locks to circuits.""" 2 | import random 3 | from itertools import product, zip_longest 4 | from random import choice, choices, randint, sample, shuffle 5 | 6 | import circuitgraph as cg 7 | 8 | 9 | def trll(c, keylen, s1_s2_ratio=1, shuffle_key=True, seed=None): 10 | """Locks a circuitgraph with Truly Random Logic Locking. 11 | 12 | Limaye, E. Kalligeros, N. Karousos, I. G. Karybali and O. Sinanoglu, 13 | "Thwarting All Logic Locking Attacks: Dishonest Oracle With Truly Random 14 | Logic Locking," in IEEE Transactions on Computer-Aided Design of Integrated 15 | Circuits and Systems, vol. 40, no. 9, pp. 1740-1753, Sept. 2021. 16 | 17 | Parameters 18 | ---------- 19 | c: circuitgraph.Circuit 20 | The circuit to lock. 21 | keylen: int 22 | The number of key bits to add. 23 | s1_s2_ratio: int or str 24 | The ratio between number of key gate locations locked where an 25 | inverter exists in the original design (s1) or where an inverter 26 | does not exist in the original design (s2). The paper leaves this 27 | value at 1 (meaning s1=s2=keylen/2), but they note that this 28 | could be adjusted based on the ratio of the locations where there 29 | is an inverter in the original netlist. Setting this parameter 30 | to the string "infer" will do this adjustment. I.e. 31 | s1 = keylen*r, s2 = keylen*(1-r), where r is the number of 32 | inverters in the circuit divided by the total number of gates. 33 | shuffle_key: bool 34 | By default, the key input labels are shuffled at the end of the 35 | algorithm so the labelling does not reveal which portion of the 36 | algorithm the key input was added during. 37 | seed: int 38 | Seed for the random selection of gates and shuffling of the key. 39 | 40 | Returns 41 | ------- 42 | circuitgraph.Circuit, dict of str:bool 43 | The locked circuit and the correct key value for each key input. 44 | """ 45 | rng = random.Random(seed) 46 | 47 | cl = c.copy() 48 | 49 | if keylen % 2 != 0: 50 | raise NotImplementedError 51 | if s1_s2_ratio == "infer": 52 | raise NotImplementedError 53 | 54 | s1 = int((keylen // 2) * s1_s2_ratio) 55 | if s1 > keylen: 56 | raise ValueError(f"Unusable s1_s2_ratio: {s1_s2_ratio}") 57 | s2 = keylen - s1 58 | 59 | s1a = rng.randint(0, s1) 60 | s1b = s1 - s1a 61 | 62 | s2a = rng.randint(0, s2) 63 | s2b = s2 - s2a 64 | 65 | inv_gates = list(c.filter_type("not")) 66 | rng.shuffle(inv_gates) 67 | rem_gates = list( 68 | c.nodes() 69 | - c.io() 70 | - c.filter_type(("not", "bb_input", "bb_output", "0", "1", "x")) 71 | ) 72 | rng.shuffle(rem_gates) 73 | 74 | j = 0 75 | k = {} 76 | # Replace existing inv_gates with XOR key-gates 77 | for _ in range(s1a): 78 | sel_gate = inv_gates.pop() 79 | ki = f"key_{j}" 80 | cl.add(ki, "input") 81 | k[ki] = True 82 | cl.set_type(sel_gate, "xor") 83 | cl.connect(ki, sel_gate) 84 | j += 1 85 | 86 | # Add XOR key-gates before existing inv_gates 87 | for _ in range(s1b): 88 | sel_gate = inv_gates.pop() 89 | ki = f"key_{j}" 90 | cl.add(ki, "input") 91 | k[ki] = False 92 | inv_fanin = cl.fanin(sel_gate) 93 | assert len(inv_fanin) == 1 94 | cl.disconnect(inv_fanin, sel_gate) 95 | cl.add(f"key_gate_{j}", "xor", fanin=inv_fanin | {ki}, fanout=sel_gate) 96 | j += 1 97 | 98 | # Add XOR key-gates and INV gates after existing rem_gates 99 | for _ in range(s2a): 100 | sel_gate = rem_gates.pop() 101 | ki = f"key_{j}" 102 | cl.add(ki, "input") 103 | k[ki] = True 104 | sel_fanout = cl.fanout(sel_gate) 105 | cl.disconnect(sel_gate, sel_fanout) 106 | cl.add(f"key_gate_{j}", "xor", fanin=(sel_gate, ki)) 107 | cl.add(f"key_inv_{j}", "not", fanin=f"key_gate_{j}", fanout=sel_fanout) 108 | j += 1 109 | 110 | # Add XOR key-gates after existing rem_gates 111 | for _ in range(s2b): 112 | sel_gate = rem_gates.pop() 113 | ki = f"key_{j}" 114 | cl.add(ki, "input") 115 | k[ki] = False 116 | sel_fanout = cl.fanout(sel_gate) 117 | cl.disconnect(sel_gate, sel_fanout) 118 | cl.add(f"key_gate_{j}", "xor", fanin=(sel_gate, ki), fanout=sel_fanout) 119 | j += 1 120 | 121 | # Shuffle keys 122 | if shuffle_key: 123 | new_order = list(range(keylen)) 124 | rng.shuffle(new_order) 125 | shuffled_k = {} 126 | intermediate_mapping = {} 127 | final_mapping = {} 128 | for old_idx, new_idx in enumerate(new_order): 129 | shuffled_k[f"key_{new_idx}"] = k[f"key_{old_idx}"] 130 | intermediate_mapping[f"key_{old_idx}"] = f"key_{old_idx}_temp" 131 | final_mapping[f"key_{old_idx}_temp"] = f"key_{new_idx}" 132 | cl.relabel(intermediate_mapping) 133 | cl.relabel(final_mapping) 134 | return cl, shuffled_k 135 | return cl, k 136 | 137 | 138 | def xor_lock(c, keylen, key_prefix="key_", replacement=False): 139 | """Locks a circuitgraph with a random xor lock. 140 | 141 | J. A. Roy, F. Koushanfar and I. L. Markov, "Ending Piracy of Integrated 142 | Circuits," in Computer, vol. 43, no. 10, pp. 30-38, Oct. 2010. 143 | 144 | Parameters 145 | ---------- 146 | c: circuitgraph.CircuitGraph 147 | Circuit to lock. 148 | keylen: int 149 | the number of bits in the key 150 | replacement: bool 151 | If True, the same line can be locked twice (resulting in a chain 152 | of key gates) 153 | 154 | Returns 155 | ------- 156 | circuitgraph.CircuitGraph, dict of str:bool 157 | the locked circuit and the correct key value for each key input 158 | """ 159 | # create copy to lock 160 | cl = c.copy() 161 | 162 | # randomly select gates to lock 163 | if replacement: 164 | gates = choices(tuple(cl.nodes() - cl.outputs()), k=keylen) 165 | else: 166 | gates = sample(tuple(cl.nodes() - cl.outputs()), keylen) 167 | 168 | # insert key gates 169 | key = {} 170 | for i, gate in enumerate(gates): 171 | # select random key value 172 | key[f"{key_prefix}{i}"] = choice([True, False]) 173 | 174 | # create xor/xnor,input 175 | gate_type = "xnor" if key[f"{key_prefix}{i}"] else "xor" 176 | fanout = cl.fanout(gate) 177 | cl.disconnect(gate, fanout) 178 | cl.add(f"key_gate_{i}", gate_type, fanin=gate, fanout=fanout) 179 | cl.add(f"{key_prefix}{i}", "input", fanout=f"key_gate_{i}") 180 | 181 | cg.lint(cl) 182 | return cl, key 183 | 184 | 185 | def mux_lock(c, keylen, avoid_loops=False, key_prefix="key_"): 186 | """Locks a circuitgraph with a mux lock. 187 | 188 | J. Rajendran et al., "Fault Analysis-Based Logic Encryption," in IEEE 189 | Transactions on Computers, vol. 64, no. 2, pp. 410-424, Feb. 2015, 190 | doi: 10.1109/TC.2013.193. 191 | 192 | Note that a random mux selection is used, not fault-based. 193 | 194 | Parameters 195 | ---------- 196 | c: circuitgraph.CircuitGraph 197 | Circuit to lock. 198 | keylen: int 199 | the number of bits in the key. 200 | 201 | Returns 202 | ------- 203 | circuitgraph.CircuitGraph, dict of str:bool 204 | the locked circuit and the correct key value for each key input 205 | """ 206 | # create copy to lock 207 | cl = c.copy() 208 | 209 | # get 2:1 mux 210 | m = cg.logic.mux(2) 211 | 212 | # randomly select gates 213 | gates = sample(tuple(cl.nodes() - cl.outputs()), keylen) 214 | if avoid_loops: 215 | decoy_gates = set() 216 | else: 217 | decoy_gates = sample(tuple(cl.nodes() - cl.outputs()), keylen) 218 | 219 | # insert key gates 220 | key = {} 221 | for i, gate in enumerate(gates): 222 | # select random key value 223 | key_val = choice([True, False]) 224 | 225 | if avoid_loops: 226 | decoy_gate = choice( 227 | tuple( 228 | c.nodes() 229 | - c.io() 230 | - set(gates) 231 | - cl.transitive_fanout(gate) 232 | - decoy_gates 233 | ) 234 | ) 235 | decoy_gates.add(decoy_gate) 236 | else: 237 | decoy_gate = decoy_gates[i] 238 | 239 | # create and connect mux 240 | fanout = cl.fanout(gate) 241 | cl.disconnect(gate, fanout) 242 | cl.add_subcircuit(m, f"mux_{i}") 243 | cl.connect(f"mux_{i}_out", fanout) 244 | key_in = cl.add(f"{key_prefix}{i}", "input", fanout=f"mux_{i}_sel_0", uid=True) 245 | key[key_in] = key_val 246 | if key_val: 247 | cl.connect(gate, f"mux_{i}_in_1") 248 | cl.connect(decoy_gate, f"mux_{i}_in_0") 249 | else: 250 | cl.connect(gate, f"mux_{i}_in_0") 251 | cl.connect(decoy_gate, f"mux_{i}_in_1") 252 | 253 | cg.lint(cl) 254 | if avoid_loops and cl.is_cyclic(): 255 | raise ValueError("Locked circuit is cyclic") 256 | return cl, key 257 | 258 | 259 | def random_lut_lock(c, num_gates, lut_width, gates=None): 260 | """Locks a circuitgraph by replacing random gates with LUTs. 261 | 262 | This is kind of like applying LUT-lock with no replacement strategy. 263 | (H. Mardani Kamali, K. Zamiri Azar, K. Gaj, H. Homayoun and A. Sasan, 264 | "LUT-Lock: A Novel LUT-Based Logic Obfuscation for FPGA-Bitstream and 265 | ASIC-Hardware Protection," 2018 IEEE Computer Society Annual Symposium on 266 | VLSI (ISVLSI), Hong Kong, 2018, pp. 405-410.) 267 | 268 | Parameters 269 | ---------- 270 | circuit: circuitgraph.CircuitGraph 271 | Circuit to lock. 272 | num_gates: int 273 | the number of gates to lock. 274 | lut_width: int 275 | LUT width, defines maximum fanin of locked gates. 276 | gates: list of str 277 | The gates to lock. If not provided, will be randomly sampled 278 | 279 | Returns 280 | ------- 281 | circuitgraph.CircuitGraph, dict of str:bool 282 | the locked circuit and the correct key value for each key input 283 | """ 284 | # create copy to lock 285 | cl = c.copy() 286 | 287 | # parse mux 288 | m = cg.logic.mux(2**lut_width) 289 | 290 | # randomly select gates 291 | potential_gates = {g for g in cl.nodes() - cl.io() if len(cl.fanin(g)) <= lut_width} 292 | if gates: 293 | if len(gates) != num_gates: 294 | raise ValueError( 295 | f"Got 'num_gates' of {num_gates} but length of " 296 | f"'gates' is {len(gates)}" 297 | ) 298 | if any(len(cl.fanin(g)) > lut_width for g in gates): 299 | raise ValueError("cannot lock a gate with fanin greater than " "lut_width") 300 | if any(g in cl.io() for g in gates): 301 | raise ValueError("cannot lock an input/output gate") 302 | else: 303 | gates = sample(tuple(potential_gates), num_gates) 304 | potential_gates -= set(gates) 305 | potential_gates -= cl.transitive_fanout(gates) 306 | 307 | # insert key gates 308 | key = {} 309 | for i, gate in enumerate(gates): 310 | fanout = list(cl.fanout(gate)) 311 | fanin = list(cl.fanin(gate)) 312 | try: 313 | padding = sample( 314 | tuple(potential_gates - cl.fanin(gate)), lut_width - len(fanin) 315 | ) 316 | except ValueError as e: 317 | raise ValueError("Could not find enough viable gates for padding") from e 318 | 319 | # create LUT 320 | cl.add_subcircuit(m, f"lut_{i}") 321 | 322 | # connect keys 323 | for j, vs in enumerate(product([False, True], repeat=len(fanin + padding))): 324 | assumptions = { 325 | s: v for s, v in zip(fanin + padding, vs[::-1]) if s in fanin 326 | } 327 | key_in = cl.add( 328 | f"key_{i*2**lut_width+j}", "input", fanout=f"lut_{i}_in_{j}", uid=True 329 | ) 330 | result = cg.sat.solve(c, assumptions) 331 | if not result: 332 | key[key_in] = False 333 | else: 334 | key[key_in] = result[gate] 335 | 336 | # connect out 337 | cl.disconnect(gate, fanout) 338 | cl.connect(f"lut_{i}_out", fanout) 339 | 340 | # connect sel 341 | for j, f in enumerate(fanin + padding): 342 | cl.connect(f, f"lut_{i}_sel_{j}") 343 | 344 | # delete gate 345 | cl.remove(gate) 346 | cl = cg.tx.relabel(cl, {f"lut_{i}_out": gate}) 347 | 348 | cg.lint(cl) 349 | return cl, key 350 | 351 | 352 | def lut_lock( 353 | c, 354 | num_gates, 355 | count_keys=False, 356 | skip_fi1=True, 357 | rank_by_shared_fanin=False, 358 | key_prefix="key_", 359 | ): 360 | """Locks a circuitgraph with NB2-MO-HSC LUT-lock. 361 | 362 | H. Mardani Kamali, K. Zamiri Azar, K. Gaj, H. Homayoun and A. Sasan, 363 | "LUT-Lock: A Novel LUT-Based Logic Obfuscation for FPGA-Bitstream and 364 | ASIC-Hardware Protection," 2018 IEEE Computer Society Annual Symposium on 365 | VLSI (ISVLSI), Hong Kong, 2018, pp. 405-410. 366 | 367 | Parameters 368 | ---------- 369 | circuit: circuitgraph.CircuitGraph 370 | Circuit to lock. 371 | num_gates: int 372 | The number of gates to lock. 373 | count_keys: bool 374 | If true, continue locking until at least `num_gates` keys are 375 | added instead of `num_gates` gates. 376 | skip_fi1: int 377 | If True, nodes with a fanin of 1 (i.e. buf or inv) will not 378 | be considered for locking. 379 | rank_by_shared_fanin: bool 380 | If True, the output with the least shared fanin with other outputs 381 | will be selected for locking first. By default, the output with 382 | the least amount of total fanin is selected for locking first. 383 | 384 | Returns 385 | ------- 386 | circuitgraph.CircuitGraph, dict of str:bool 387 | the locked circuit and the correct key value for each key input 388 | 389 | Raises 390 | ------ 391 | ValueError 392 | if there are not enough viable gates to lock. 393 | """ 394 | # create copy to lock 395 | cl = c.copy() 396 | 397 | def calc_skew(gate, cl): 398 | d = {False: 0, True: 0} 399 | fanin = list(cl.fanin(gate)) 400 | 401 | # create subcircuit containing just gate for simulation 402 | simc = cg.Circuit() 403 | for i in fanin: 404 | simc.add(i, "input") 405 | simc.add(gate, cl.type(gate), fanin=fanin) 406 | 407 | # simulate 408 | for i, vs in enumerate(product([False, True], repeat=len(fanin))): 409 | assumptions = dict(zip(fanin, vs[::-1])) 410 | result = cg.sat.solve(simc, assumptions) 411 | if not result: 412 | d[False] += 1 413 | else: 414 | d[result[gate]] += 1 415 | num_combos = 2 ** len(fanin) 416 | return abs(d[False] / num_combos - d[True] / num_combos) 417 | 418 | def replace_lut(gate, cl): 419 | key = {} 420 | m = cg.logic.mux(2 ** len(cl.fanin(gate))) 421 | fanout = list(cl.fanout(gate)) 422 | fanin = list(cl.fanin(gate)) 423 | 424 | # create LUT 425 | cl.add_subcircuit(m, f"lut_{gate}") 426 | 427 | # create subcircuit containing just gate for simulation 428 | simc = cg.Circuit() 429 | for i in fanin: 430 | simc.add(i, "input") 431 | simc.add(gate, cl.type(gate), fanin=fanin) 432 | 433 | # connect keys 434 | for i, vs in enumerate(product([False, True], repeat=len(fanin))): 435 | assumptions = dict(zip(fanin, vs[::-1])) 436 | cl.add(f"{key_prefix}{gate}_{i}", "input", fanout=f"lut_{gate}_in_{i}") 437 | result = cg.sat.solve(simc, assumptions) 438 | if not result: 439 | key[f"{key_prefix}{gate}_{i}"] = False 440 | else: 441 | key[f"{key_prefix}{gate}_{i}"] = result[gate] 442 | 443 | # connect out 444 | cl.disconnect(gate, fanout) 445 | cl.connect(f"lut_{gate}_out", fanout) 446 | 447 | # connect sel 448 | for i, f in enumerate(fanin): 449 | cl.connect(f, f"lut_{gate}_sel_{i}") 450 | 451 | # delete gate 452 | cl.remove(gate) 453 | return key, [f"lut_{gate}_{n}" for n in m.nodes()], f"lut_{gate}_out" 454 | 455 | def continue_locking(locked_gates, num_gates, keys, count_keys): 456 | if count_keys: 457 | return len(keys) < num_gates 458 | return locked_gates < num_gates 459 | 460 | locked_gates = 0 461 | outputs = list(cl.outputs()) 462 | if rank_by_shared_fanin: 463 | 464 | def rank_output(x): 465 | other_outputs = [o for o in outputs if o != x] 466 | other_fanin = cl.transitive_fanin(other_outputs) 467 | curr_fanin = cl.transitive_fanin(x) 468 | return len(curr_fanin & other_fanin) 469 | 470 | else: 471 | 472 | def rank_output(x): 473 | return len(cl.transitive_fanin(x)) 474 | 475 | outputs.sort(key=rank_output) 476 | candidates = [] 477 | forbidden_nodes = set() 478 | keys = {} 479 | while continue_locking(locked_gates, num_gates, keys, count_keys): 480 | if not candidates: 481 | outputs = [o for o in outputs if o not in forbidden_nodes] 482 | try: 483 | candidates.append(outputs.pop(0)) 484 | except IndexError as e: 485 | raise ValueError( 486 | "Ran out of candidate gates at " f"{locked_gates} gates." 487 | ) from e 488 | else: 489 | candidate = candidates.pop(0) 490 | candidate_is_output = cl.is_output(candidate) 491 | children = cl.fanin(candidate) 492 | if candidate in forbidden_nodes: 493 | candidates += [g for g in children if g not in forbidden_nodes] 494 | continue 495 | forbidden_nodes.add(candidate) 496 | if len(children) == 0: 497 | continue 498 | if skip_fi1 and len(children) == 1: 499 | child = children.pop() 500 | if child not in forbidden_nodes | set(candidates): 501 | candidates.insert(0, child) 502 | continue 503 | key, nodes, output_to_relabel = replace_lut(candidate, cl) 504 | keys.update(key) 505 | forbidden_nodes.update(nodes) 506 | cl = cg.tx.relabel(cl, {output_to_relabel: candidate}) 507 | if candidate_is_output: 508 | cl.set_output(candidate) 509 | for g1 in children: 510 | forbidden_nodes.add(g1) 511 | for g2 in cl.fanin(g1): 512 | if g2 not in forbidden_nodes | set(candidates): 513 | candidates.append(g2) 514 | # Sort by least number of outputs in fanout cone, then most skew 515 | candidates.sort( 516 | key=lambda x: ( 517 | len(cl.transitive_fanout(x) & cl.outputs()), 518 | -calc_skew(x, cl), 519 | ) 520 | ) 521 | locked_gates += 1 522 | 523 | return cl, keys 524 | 525 | 526 | def tt_lock(c, width, target_output=None): 527 | """Locks a circuitgraph with TTLock. 528 | 529 | Note that in this implementation the original circuit is not 530 | functionally stripped, meaning that it does not produce an inverted 531 | response for the protected input pattern. This makes this implementation 532 | vulnerable to removal attacks. However, it can still be used to measure 533 | SAT attack resilience. 534 | 535 | M. Yasin, A. Sengupta, B. Schafer, Y. Makris, O. Sinanoglu, and 536 | J. Rajendran, “What to Lock?: Functional and Parametric Locking,” 537 | in Great Lakes Symposium on VLSI, pp. 351–356, 2017. 538 | 539 | Parameters 540 | ---------- 541 | c: circuitgraph.CircuitGraph 542 | Circuit to lock. 543 | width: int 544 | the minimum fanin of the gates to lock. 545 | target_output: str 546 | If defined, this output will be the one which is locked. 547 | Otherwise, a random output will be locked. 548 | 549 | Returns 550 | ------- 551 | circuitgraph.CircuitGraph, dict of str:bool 552 | the locked circuit and the correct key value for each key input 553 | """ 554 | # create copy to lock 555 | cl = c.copy() 556 | 557 | if len(c.inputs()) < width: 558 | raise ValueError(f"Not enough inputs to lock with width '{width}'") 559 | 560 | if not target_output: 561 | target_output = random.choice(list(cl.outputs())) 562 | 563 | # get inputs to lock 564 | target_inputs = cl.startpoints(target_output) 565 | if len(target_inputs) < width: 566 | target_inputs |= set( 567 | random.sample(list(cl.inputs() - target_inputs), width - len(target_inputs)) 568 | ) 569 | target_inputs = list(target_inputs) 570 | 571 | # create key 572 | key = {f"key_{i}": random.choice([True, False]) for i in range(width)} 573 | 574 | # connect comparators 575 | cl.add("flip_out", "and") 576 | cl.add("restore_out", "and") 577 | for i, inp in enumerate(random.sample(target_inputs, width)): 578 | cl.add(f"key_{i}", "input") 579 | cl.add(f"hardcoded_key_{i}", "1" if key[f"key_{i}"] else "0") 580 | cl.add(f"restore_xor_{i}", "xor", fanin=[f"key_{i}", inp], fanout="restore_out") 581 | cl.add( 582 | f"flip_xor_{i}", "xor", fanin=[f"hardcoded_key_{i}", inp], fanout="flip_out" 583 | ) 584 | 585 | # flip output 586 | old_out = cl.uid(f"{target_output}_pre_lock") 587 | cl = cg.tx.relabel(cl, {target_output: old_out}) 588 | cl.set_output(old_out, False) 589 | cl.add( 590 | target_output, "xor", fanin=[old_out, "restore_out", "flip_out"], output=True 591 | ) 592 | 593 | cg.lint(cl) 594 | return cl, key 595 | 596 | 597 | def tt_lock_sen(c, width, nsamples=10): 598 | """Locks a circuitgraph with TTLock-Sen. 599 | 600 | Joseph Sweeney, Marijn J.H. Heule, and Lawrence Pileggi, 601 | “Sensitivity Analysis of Locked Circuits,” in 602 | Logic for Programming, Artificial Intelligence and Reasoning 603 | (LPAR-23), pp. 483-497. EPiC Series in Computing 73, EasyChair. 604 | 605 | Parameters 606 | ---------- 607 | c: circuitgraph.CircuitGraph 608 | Circuit to lock. 609 | width: int 610 | the minimum fanin of the gates to lock. 611 | 612 | Returns 613 | ------- 614 | circuitgraph.CircuitGraph, dict of str:bool 615 | the locked circuit and the correct key value for each key input 616 | """ 617 | # create copy to lock 618 | cl = c.copy() 619 | 620 | # find output with large enough fanin 621 | potential_outs = [o for o in cl.outputs() if len(cl.startpoints(o)) >= width] 622 | if not potential_outs: 623 | raise ValueError(f"Not enough inputs to lock with width '{width}'") 624 | 625 | # find average sensitivities 626 | A = {} 627 | N = {} 628 | S = {} 629 | for o in potential_outs: 630 | # build sensitivity circuit 631 | s = cg.tx.sensitivity_transform(c, o) 632 | startpoints = c.startpoints(o) 633 | s_out = {o for o in s.outputs() if "difference" in o} 634 | 635 | # est avg sensitivity 636 | total = 0 637 | for i in range(nsamples): 638 | input_val = {i: randint(0, 1) for i in startpoints} 639 | model = cg.sat.solve(s, input_val) 640 | sen = sum(model[o] for o in s_out) 641 | total += sen 642 | A[o] = int(total / nsamples) 643 | N[o] = len(startpoints) 644 | S[o] = s 645 | 646 | # find output + input value with closest to avg sen 647 | def find_input(): 648 | b = 0 649 | while b < max(N.values()): 650 | for o in potential_outs: 651 | upper = min(N[o], int(N[o] - A[o] + b)) 652 | lower = max(0, int(N[o] - A[o] - b)) 653 | us = cg.utils.int_to_bin(upper, cg.utils.clog2(N[o])) 654 | ls = cg.utils.int_to_bin(lower, cg.utils.clog2(N[o])) 655 | for sv in [us, ls]: 656 | model = cg.sat.solve( 657 | S[o], {f"sen_out_{i}": v for i, v in enumerate(sv)} 658 | ) 659 | if model: 660 | out = o 661 | startpoints = c.startpoints(o) 662 | 663 | key = {f"key_{i}": model[n] for i, n in enumerate(startpoints)} 664 | return key, startpoints, out 665 | b += 1 666 | 667 | key, startpoints, out = find_input() 668 | 669 | # connect comparators 670 | cl.add("flip_out", "and") 671 | cl.add("restore_out", "and") 672 | for i, inp in enumerate(startpoints): 673 | cl.add(f"key_{i}", "input") 674 | cl.add(f"hardcoded_key_{i}", "1" if key[f"key_{i}"] else "0") 675 | cl.add(f"restore_xor_{i}", "xor", fanin=[f"key_{i}", inp], fanout="restore_out") 676 | cl.add( 677 | f"flip_xor_{i}", "xor", fanin=[f"hardcoded_key_{i}", inp], fanout="flip_out" 678 | ) 679 | 680 | # flip output 681 | old_out = cl.uid(f"{out}_pre_lock") 682 | cl = cg.tx.relabel(cl, {out: old_out}) 683 | cl.set_output(old_out, False) 684 | cl.add(out, "xor", fanin=[old_out, "restore_out", "flip_out"], output=True) 685 | 686 | cg.lint(cl) 687 | return cl, key 688 | 689 | 690 | def sfll_hd(c, width, hd, target_output=None): 691 | """Locks a circuitgraph with SFLL-HD. 692 | 693 | Note that in this implementation the original circuit is not 694 | functionally stripped, meaning that it does not produce an inverted 695 | response for the protected input pattern. This makes this implementation 696 | vulnerable to removal attacks. However, it can still be used to measure 697 | SAT attack resilience. 698 | 699 | Muhammad Yasin, Abhrajit Sengupta, Mohammed Thari Nabeel, Mohammed Ashraf, 700 | Jeyavijayan (JV) Rajendran, and Ozgur Sinanoglu. 2017. Provably-Secure 701 | Logic Locking: From Theory To Practice. In Proceedings of the 2017 ACM 702 | SIGSAC Conference on Computer and Communications Security (CCS ’17). 703 | Association for Computing Machinery, New York, NY, USA, 1601–1618. 704 | 705 | Parameters 706 | ---------- 707 | c: circuitgraph.CircuitGraph 708 | Circuit to lock. 709 | width: int 710 | key width, also the minimum fanin of the gates to lock. 711 | hd: int 712 | the hamming distance to lock with, as explained in the paper. 713 | target_output: str 714 | If defined, this output will be the one which is locked. 715 | Otherwise, a random output will be locked. 716 | 717 | Returns 718 | ------- 719 | circuitgraph.CircuitGraph, dict of str:bool 720 | the locked circuit and the correct key value for each key input 721 | """ 722 | # create copy to lock 723 | cl = c.copy() 724 | 725 | # parse popcount 726 | p = cg.logic.popcount(width) 727 | 728 | if len(c.inputs()) < width: 729 | raise ValueError(f"Not enough inputs to lock with width '{width}'") 730 | 731 | if not target_output: 732 | target_output = random.choice(list(cl.outputs())) 733 | 734 | # get inputs to lock 735 | target_inputs = cl.startpoints(target_output) 736 | if len(target_inputs) < width: 737 | target_inputs |= set( 738 | random.sample(list(cl.inputs() - target_inputs), width - len(target_inputs)) 739 | ) 740 | target_inputs = list(target_inputs) 741 | 742 | # create key 743 | key = {f"key_{i}": random.choice([True, False]) for i in range(width)} 744 | 745 | # instantiate and connect hd circuits 746 | cl.add_subcircuit(p, "flip_pop") 747 | cl.add_subcircuit(p, "restore_pop") 748 | 749 | # connect inputs 750 | for i, inp in enumerate(random.sample(target_inputs, width)): 751 | cl.add(f"key_{i}", "input") 752 | cl.add(f"hardcoded_key_{i}", "1" if key[f"key_{i}"] else "0") 753 | cl.add(f"restore_xor_{i}", "xor", fanin=[f"key_{i}", inp]) 754 | cl.add(f"flip_xor_{i}", "xor", fanin=[f"hardcoded_key_{i}", inp]) 755 | cl.connect(f"flip_xor_{i}", f"flip_pop_in_{i}") 756 | cl.connect(f"restore_xor_{i}", f"restore_pop_in_{i}") 757 | 758 | # connect outputs 759 | cl.add("flip_out", "and") 760 | cl.add("restore_out", "and") 761 | for i, v in enumerate(format(hd, f"0{cg.utils.clog2(width)+1}b")[::-1]): 762 | cl.add(f"hd_{i}", v) 763 | cl.add( 764 | f"restore_out_xnor_{i}", 765 | "xnor", 766 | fanin=[f"hd_{i}", f"restore_pop_out_{i}"], 767 | fanout="restore_out", 768 | ) 769 | cl.add( 770 | f"flip_out_xnor_{i}", 771 | "xnor", 772 | fanin=[f"hd_{i}", f"flip_pop_out_{i}"], 773 | fanout="flip_out", 774 | ) 775 | 776 | # flip output 777 | old_out = cl.uid(f"{target_output}_pre_lock") 778 | cl = cg.tx.relabel(cl, {target_output: old_out}) 779 | cl.set_output(old_out, False) 780 | cl.add( 781 | target_output, "xor", fanin=[old_out, "restore_out", "flip_out"], output=True 782 | ) 783 | 784 | cg.lint(cl) 785 | return cl, key 786 | 787 | 788 | def sfll_flex(c, width, n, target_output=None): 789 | """Locks a circuitgraph with SFLL-flex. 790 | 791 | Note that in this implementation the original circuit is not 792 | functionally stripped, meaning that it does not produce an inverted 793 | response for the protected input pattern. This makes this implementation 794 | vulnerable to removal attacks. However, it can still be used to measure 795 | SAT attack resilience. 796 | 797 | Muhammad Yasin, Abhrajit Sengupta, Mohammed Thari Nabeel, 798 | Mohammed Ashraf, Jeyavijayan (JV) Rajendran, and Ozgur Sinanoglu. 2017. 799 | Provably-Secure Logic Locking: From Theory To Practice. In Proceedings of 800 | the 2017 ACM SIGSAC Conference on Computer and Communications Security. 801 | Association for Computing Machinery, New York, NY, USA, 1601–1618. 802 | 803 | Parameters 804 | ---------- 805 | c: circuitgraph.CircuitGraph 806 | Circuit to lock. 807 | width: int 808 | the minimum fanin of the gates to lock. 809 | n: int 810 | number of input patterns to lock. 811 | target_output: str 812 | If defined, this output will be the one which is locked. 813 | Otherwise, a random output will be locked. 814 | 815 | Returns 816 | ------- 817 | circuitgraph.CircuitGraph, dict of str:bool 818 | the locked circuit and the correct key value for each key input 819 | """ 820 | # create copy to lock 821 | cl = c.copy() 822 | 823 | if not target_output: 824 | target_output = random.choice(list(cl.outputs())) 825 | 826 | # get inputs to lock 827 | target_inputs = cl.startpoints(target_output) 828 | if len(target_inputs) < width: 829 | target_inputs |= set( 830 | random.sample(list(cl.inputs() - target_inputs), width - len(target_inputs)) 831 | ) 832 | target_inputs = list(target_inputs) 833 | 834 | # create key 835 | key = {f"key_{i}": choice([True, False]) for i in range(width * n)} 836 | 837 | # connect comparators 838 | cl.add("flip_out", "or") 839 | cl.add("restore_out", "or") 840 | 841 | for j in range(n): 842 | cl.add(f"flip_and_{j}", "and", fanout="flip_out") 843 | cl.add(f"restore_and_{j}", "and", fanout="restore_out") 844 | 845 | for i, inp in enumerate(random.sample(target_inputs, width)): 846 | for j in range(n): 847 | cl.add(f"key_{i+j*width}", "input") 848 | cl.add(f"hardcoded_key_{i}_{j}", "1" if key[f"key_{i+j*width}"] else "0") 849 | cl.add( 850 | f"restore_xor_{i}_{j}", 851 | "xor", 852 | fanin=[f"key_{i+j*width}", inp], 853 | fanout=f"restore_and_{j}", 854 | ) 855 | cl.add( 856 | f"flip_xor_{i}_{j}", 857 | "xor", 858 | fanin=[f"hardcoded_key_{i}_{j}", inp], 859 | fanout=f"flip_and_{j}", 860 | ) 861 | 862 | # flip output 863 | old_out = cl.uid(f"{target_output}_pre_lock") 864 | cl = cg.tx.relabel(cl, {target_output: old_out}) 865 | cl.set_output(old_out, False) 866 | cl.add( 867 | target_output, "xor", fanin=[old_out, "restore_out", "flip_out"], output=True 868 | ) 869 | 870 | cg.lint(cl) 871 | return cl, key 872 | 873 | 874 | def _connect_banyan(cl, swb_ins, swb_outs, bw): 875 | I = int(2 * cg.utils.clog2(bw) - 2) 876 | J = int(bw / 2) 877 | for i in range(cg.utils.clog2(J)): 878 | r = J / (2**i) 879 | for j in range(J): 880 | t = (j % r) >= (r / 2) 881 | # straight 882 | out_i = int((i * bw) + (2 * j) + t) 883 | in_i = int((i * bw + bw) + (2 * j) + t) 884 | cl.connect(swb_outs[out_i], swb_ins[in_i]) 885 | 886 | # cross 887 | out_i = int((i * bw) + (2 * j) + (1 - t) + ((r - 1) * ((1 - t) * 2 - 1))) 888 | in_i = int((i * bw + bw) + (2 * j) + (1 - t)) 889 | cl.connect(swb_outs[out_i], swb_ins[in_i]) 890 | 891 | if r > 2: 892 | # straight 893 | out_i = int(((I * J * 2) - ((2 + i) * bw)) + (2 * j) + t) 894 | in_i = int(((I * J * 2) - ((1 + i) * bw)) + (2 * j) + t) 895 | cl.connect(swb_outs[out_i], swb_ins[in_i]) 896 | 897 | # cross 898 | out_i = int( 899 | ((I * J * 2) - ((2 + i) * bw)) 900 | + (2 * j) 901 | + (1 - t) 902 | + ((r - 1) * ((1 - t) * 2 - 1)) 903 | ) 904 | in_i = int(((I * J * 2) - ((1 + i) * bw)) + (2 * j) + (1 - t)) 905 | cl.connect(swb_outs[out_i], swb_ins[in_i]) 906 | 907 | 908 | def _connect_banyan_bb(cl, swb_ins, swb_outs, bw): 909 | I = int(2 * cg.utils.clog2(bw) - 2) 910 | J = int(bw / 2) 911 | for i in range(cg.utils.clog2(J)): 912 | r = J / (2**i) 913 | for j in range(J): 914 | t = (j % r) >= (r / 2) 915 | # straight 916 | out_i = int((i * bw) + (2 * j) + t) 917 | in_i = int((i * bw + bw) + (2 * j) + t) 918 | cl.add( 919 | f"swb_{i}_{j}_straight", 920 | "buf", 921 | fanin=swb_outs[out_i], 922 | fanout=swb_ins[in_i], 923 | ) 924 | 925 | # cross 926 | out_i = int((i * bw) + (2 * j) + (1 - t) + ((r - 1) * ((1 - t) * 2 - 1))) 927 | in_i = int((i * bw + bw) + (2 * j) + (1 - t)) 928 | cl.add( 929 | f"swb_{i}_{j}_cross", "buf", fanin=swb_outs[out_i], fanout=swb_ins[in_i] 930 | ) 931 | 932 | if r > 2: 933 | # straight 934 | out_i = int(((I * J * 2) - ((2 + i) * bw)) + (2 * j) + t) 935 | in_i = int(((I * J * 2) - ((1 + i) * bw)) + (2 * j) + t) 936 | cl.add( 937 | f"swb_{i}_{j}_r_straight", 938 | "buf", 939 | fanin=swb_outs[out_i], 940 | fanout=swb_ins[in_i], 941 | ) 942 | 943 | # cross 944 | out_i = int( 945 | ((I * J * 2) - ((2 + i) * bw)) 946 | + (2 * j) 947 | + (1 - t) 948 | + ((r - 1) * ((1 - t) * 2 - 1)) 949 | ) 950 | in_i = int(((I * J * 2) - ((1 + i) * bw)) + (2 * j) + (1 - t)) 951 | cl.add( 952 | f"swb_{i}_{j}_r_cross", 953 | "buf", 954 | fanin=swb_outs[out_i], 955 | fanout=swb_ins[in_i], 956 | ) 957 | 958 | 959 | def full_lock(c, bw, lw, avoid_loops=False): 960 | """Locks a circuitgraph with Full-Lock. 961 | 962 | Hadi Mardani Kamali, Kimia Zamiri Azar, Houman Homayoun, 963 | and Avesta Sasan. 2019. Full-Lock: Hard Distributions of SAT instances 964 | for Obfuscating Circuits using Fully Configurable Logic and Routing 965 | Blocks. In Proceedings of the 56th Annual Design Automation Conference 966 | 2019. Association for Computing Machinery, New York, NY, USA. 967 | 968 | Parameters 969 | ---------- 970 | circuit: circuitgraph.CircuitGraph 971 | Circuit to lock. 972 | banyan_width: int 973 | Width of Banyan network to use, must follow bw = 2**n, n>1. 974 | lut_width: int 975 | Width to use for inserted LUTs, must evenly divide bw. 976 | avoid_loops: bool 977 | If True, gates fed by the Banyan network will be selected 978 | such that they do not cause combinational loops. 979 | 980 | Returns 981 | ------- 982 | circuitgraph.CircuitGraph, dict of str:bool 983 | the locked circuit and the correct key value for each key input 984 | """ 985 | # lock with luts 986 | if avoid_loops: 987 | gates = [] 988 | potential_gates = {g for g in c.nodes() - c.io() if len(c.fanin(g)) <= lw} 989 | for _ in range(int(bw / lw)): 990 | gates.append(random.choice(list(potential_gates))) 991 | potential_gates -= c.transitive_fanin(gates[-1]) 992 | potential_gates -= c.transitive_fanout(gates[-1]) 993 | if not potential_gates: 994 | raise ValueError("Could not find enough gates to make " "acyclic lock") 995 | cl, key = random_lut_lock(c, int(bw / lw), lw, gates=gates) 996 | else: 997 | cl, key = random_lut_lock(c, int(bw / lw), lw) 998 | 999 | # generate switch 1000 | m = cg.tx.strip_io(cg.logic.mux(2)) 1001 | s = cg.Circuit(name="switch") 1002 | s.add_subcircuit(m, "m0") 1003 | s.add_subcircuit(m, "m1") 1004 | s.add("in_0", "buf", fanout=["m0_in_0", "m1_in_1"]) 1005 | s.add("in_1", "buf", fanout=["m0_in_1", "m1_in_0"]) 1006 | s.add("out_0", "xor", fanin="m0_out") 1007 | s.add("out_1", "xor", fanin="m1_out") 1008 | s.add("key_0", "input", fanout=["m0_sel_0", "m1_sel_0"]) 1009 | s.add("key_1", "input", fanout="out_0") 1010 | s.add("key_2", "input", fanout="out_1") 1011 | 1012 | # generate banyan 1013 | I = int(2 * cg.utils.clog2(bw) - 2) 1014 | J = int(bw / 2) 1015 | 1016 | # add switches 1017 | for i in range(I * J): 1018 | cl.add_subcircuit(s, f"swb_{i}") 1019 | 1020 | # make connections 1021 | swb_ins = [f"swb_{i//2}_in_{i%2}" for i in range(I * J * 2)] 1022 | swb_outs = [f"swb_{i//2}_out_{i%2}" for i in range(I * J * 2)] 1023 | _connect_banyan(cl, swb_ins, swb_outs, bw) 1024 | 1025 | # get banyan io 1026 | net_ins = swb_ins[:bw] 1027 | net_outs = swb_outs[-bw:] 1028 | 1029 | # generate key 1030 | for i in range(I * J): 1031 | for j in range(3): 1032 | key[f"swb_{i}_key_{j}"] = choice([True, False]) 1033 | 1034 | # get banyan mapping 1035 | mapping = {} 1036 | polarity = {} 1037 | orig_result = cg.sat.solve(cl, {**{n: False for n in net_ins}, **key}) 1038 | for net_in in net_ins: 1039 | result = cg.sat.solve(cl, {**{n: n == net_in for n in net_ins}, **key}) 1040 | for net_out in net_outs: 1041 | if result[net_out] != orig_result[net_out]: 1042 | mapping[net_in] = net_out 1043 | polarity[net_in] = result[net_out] 1044 | break 1045 | 1046 | # connect banyan io to luts 1047 | for i in range(int(bw / lw)): 1048 | for j in range(lw): 1049 | driver = cl.fanin(f"lut_{i}_sel_{j}").pop() 1050 | cl.disconnect(driver, f"lut_{i}_sel_{j}") 1051 | net_in = net_ins[i * lw + j] 1052 | cl.connect(mapping[net_in], f"lut_{i}_sel_{j}") 1053 | if not polarity[net_in]: 1054 | driver = cl.add(f"not_{net_in}", "not", fanin=driver) 1055 | cl.connect(driver, net_in) 1056 | 1057 | for k in key: 1058 | cl.set_type(k, "input") 1059 | 1060 | cg.lint(cl) 1061 | if avoid_loops and cl.is_cyclic(): 1062 | raise ValueError("Locked circuit is cyclic") 1063 | return cl, key 1064 | 1065 | 1066 | def full_lock_mux(c, bw, lw): 1067 | """Locks a circuitgraph with a muxed-based model of Full-Lock. 1068 | 1069 | Uses muxes instead of the Banyan network, a relaxation that breaks symmetry 1070 | and simplifies the model substantially. 1071 | 1072 | Joseph Sweeney, Marijn J.H. Heule, and Lawrence Pileggi 1073 | Modeling Techniques for Logic Locking. In Proceedings 1074 | of the International Conference on Computer Aided Design 2020 (ICCAD-39). 1075 | 1076 | Parameters 1077 | ---------- 1078 | c: circuitgraph.CircuitGraph 1079 | Circuit to lock. 1080 | banyan_width: int 1081 | Width of Banyan network to use, must follow bw = 2**n, n>1. 1082 | lut_width: int 1083 | Width to use for inserted LUTs, must evenly divide bw. 1084 | 1085 | Returns 1086 | ------- 1087 | circuitgraph.CircuitGraph, dict of str:bool 1088 | the locked circuit and the correct key value for each key input 1089 | """ 1090 | # first generate banyan, to get a valid mapping for the key 1091 | b = cg.Circuit() 1092 | 1093 | # generate switch 1094 | m = cg.tx.strip_io(cg.logic.mux(2)) 1095 | s = cg.Circuit(name="switch") 1096 | s.add_subcircuit(m, "m0") 1097 | s.add_subcircuit(m, "m1") 1098 | s.add("in_0", "buf", fanout=["m0_in_0", "m1_in_1"]) 1099 | s.add("in_1", "buf", fanout=["m0_in_1", "m1_in_0"]) 1100 | s.add("out_0", "xor", fanin="m0_out") 1101 | s.add("out_1", "xor", fanin="m1_out") 1102 | s.add("key_0", "input", fanout=["m0_sel_0", "m1_sel_0"]) 1103 | s.add("key_1", "input", fanout="out_0") 1104 | s.add("key_2", "input", fanout="out_1") 1105 | 1106 | # generate banyan 1107 | I = int(2 * cg.utils.clog2(bw) - 2) 1108 | J = int(bw / 2) 1109 | 1110 | # add switches 1111 | for i in range(I * J): 1112 | b.add_subcircuit(s, f"swb_{i}") 1113 | 1114 | # make connections 1115 | swb_ins = [f"swb_{i//2}_in_{i%2}" for i in range(I * J * 2)] 1116 | swb_outs = [f"swb_{i//2}_out_{i%2}" for i in range(I * J * 2)] 1117 | _connect_banyan(b, swb_ins, swb_outs, bw) 1118 | 1119 | # get banyan io 1120 | net_ins = swb_ins[:bw] 1121 | net_outs = swb_outs[-bw:] 1122 | 1123 | # generate key 1124 | key = {} 1125 | for i in range(I * J): 1126 | for j in range(3): 1127 | key[f"swb_{i}_key_{j}"] = choice([True, False]) 1128 | 1129 | # get banyan mapping 1130 | mapping = {} 1131 | polarity = {} 1132 | orig_result = cg.sat.solve(b, {**{n: False for n in net_ins}, **key}) 1133 | for net_in in net_ins: 1134 | result = cg.sat.solve( 1135 | b, {**{n: False if n != net_in else True for n in net_ins}, **key} 1136 | ) 1137 | for net_out in net_outs: 1138 | if result[net_out] != orig_result[net_out]: 1139 | mapping[net_in] = net_out 1140 | polarity[net_in] = result[net_out] 1141 | break 1142 | 1143 | # lock with luts 1144 | cl, key = random_lut_lock(c, int(bw / lw), lw) 1145 | 1146 | # generate mux 1147 | m = cg.tx.strip_io(cg.logic.mux(bw)) 1148 | 1149 | # add muxes and xors 1150 | banyan_to_mux = {} 1151 | for i in range(bw): 1152 | cl.add_subcircuit(m, f"mux_{i}") 1153 | for b in range(cg.utils.clog2(bw)): 1154 | cl.add(f"key_{i}_{b}", "input", fanout=f"mux_{i}_sel_{b}") 1155 | cl.add(f"mux_{i}_xor", "xor", fanin=f"mux_{i}_out") 1156 | cl.add(f"key_{i}_{cg.utils.clog2(bw)}", "input", fanout=f"mux_{i}_xor") 1157 | banyan_to_mux[net_outs[i]] = f"mux_{i}_xor" 1158 | 1159 | # connect muxes to luts 1160 | for i in range(bw): 1161 | net_in = net_ins[i] 1162 | xor = banyan_to_mux[mapping[net_in]] 1163 | o = int(xor.split("_")[1]) 1164 | 1165 | driver = cl.fanin(f"lut_{i//lw}_sel_{i%lw}").pop() 1166 | cl.disconnect(driver, f"lut_{i//lw}_sel_{i%lw}") 1167 | 1168 | if not polarity[net_in]: 1169 | driver = cl.add(f"not_{net_in}", "not", fanin=driver) 1170 | key[f"key_{o}_{cg.utils.clog2(bw)}"] = True 1171 | else: 1172 | key[f"key_{o}_{cg.utils.clog2(bw)}"] = False 1173 | 1174 | for b in range(bw): 1175 | cl.connect(driver, f"mux_{b}_in_{i}") 1176 | 1177 | cl.connect(xor, f"lut_{i//lw}_sel_{i%lw}") 1178 | for b, v in enumerate(cg.utils.int_to_bin(i, cg.utils.clog2(bw), True)): 1179 | key[f"key_{o}_{b}"] = v 1180 | 1181 | cg.lint(cl) 1182 | return cl, key 1183 | 1184 | 1185 | def inter_lock(c, bw, reduced_swb=False): 1186 | """Locks a circuitgraph with InterLock. 1187 | 1188 | Kamali, Hadi Mardani, Kimia Zamiri Azar, Houman Homayoun, and Avesta Sasan. 1189 | "Interlock: An intercorrelated logic and routing locking." 1190 | In 2020 IEEE/ACM International Conference On Computer Aided Design (ICCAD), 1191 | pp. 1-9. IEEE, 2020. 1192 | 1193 | Parameters 1194 | ---------- 1195 | circuit: circuitgraph.CircuitGraph 1196 | Circuit to lock. 1197 | bw: int 1198 | The size of the keyed rounting block. A bw of m results 1199 | in an m x m sized KeyRB. 1200 | reduced_swb: bool 1201 | If True, each switchbox is reduced from 3 keys to 1 key due to the fact 1202 | that for 100% utilization, the other 2 keys will never be used. Essentially, 1203 | the muxes at the output of the switchbox are removed. 1204 | 1205 | Returns 1206 | ------- 1207 | circuitgraph.CircuitGraph, dict of str:bool 1208 | the locked circuit and the correct key value for each key input 1209 | """ 1210 | cl = c.copy() 1211 | cg.lint(cl) 1212 | 1213 | # generate switch 1214 | m = cg.tx.strip_io(cg.logic.mux(2)) 1215 | s = cg.Circuit(name="switch") 1216 | s.add_subcircuit(m, "m0") 1217 | s.add_subcircuit(m, "m1") 1218 | s.add("in_0", "input", fanout=["m0_in_0", "m1_in_1"]) 1219 | s.add("in_1", "input", fanout=["m0_in_1", "m1_in_0"]) 1220 | s.add("ex_in_0", "input") 1221 | s.add("ex_in_1", "input") 1222 | # f1 and f2 starts as 'and' gates, must be updated later 1223 | s.add("f1_out", "and", fanin=["m0_out", "ex_in_0"]) 1224 | s.add("f2_out", "and", fanin=["m1_out", "ex_in_1"]) 1225 | s.add("key_0", "input", fanout=["m0_sel_0", "m1_sel_0"]) 1226 | s.add("out_0", "buf", output=True) 1227 | s.add("out_1", "buf", output=True) 1228 | if not reduced_swb: 1229 | s.add_subcircuit(m, "m2") 1230 | s.add_subcircuit(m, "m3") 1231 | s.add("key_1", "input", fanout="m2_sel_0") 1232 | s.add("key_2", "input", fanout="m3_sel_0") 1233 | s.connect("f1_out", "m2_in_0") 1234 | s.connect("f2_out", "m3_in_1") 1235 | s.connect("m0_out", "m3_in_0") 1236 | s.connect("m1_out", "m2_in_1") 1237 | s.connect("m2_out", "out_0") 1238 | s.connect("m3_out", "out_1") 1239 | else: 1240 | s.connect("f1_out", "out_0") 1241 | s.connect("f2_out", "out_1") 1242 | 1243 | sbb_inputs = ["in_0", "in_1", "ex_in_0", "ex_in_1", "key_0"] 1244 | if not reduced_swb: 1245 | sbb_inputs += ["key_1", "key_2"] 1246 | sbb = cg.BlackBox( 1247 | "switch", 1248 | sbb_inputs, 1249 | ["out_0", "out_1"], 1250 | ) 1251 | 1252 | # Select paths to embed in the routing network 1253 | path_length = 2 * cg.utils.clog2(bw) - 2 1254 | paths = [] 1255 | 1256 | filtered_gates = set() 1257 | 1258 | def filter_gate(n): 1259 | gate = n 1260 | gates = [n] 1261 | for _ in range(path_length): 1262 | if ( 1263 | len(cl.fanin(gate)) != 2 1264 | or len(cl.fanout(gate)) != 1 1265 | or cl.is_output(gate) 1266 | or gate in filtered_gates 1267 | or len(cl.fanin(gate) & filtered_gates) > 0 1268 | or len(cl.fanout(gate) & filtered_gates) > 0 1269 | ): 1270 | return False 1271 | gate = cl.fanout(gate).pop() 1272 | gates.append(gate) 1273 | filtered_gates.update(gates) 1274 | for gate in gates: 1275 | filtered_gates.update(cl.fanin(gate)) 1276 | return True 1277 | 1278 | candidate_gates = filter(filter_gate, cl.nodes()) 1279 | for _ in range(bw): 1280 | try: 1281 | gate = next(candidate_gates) 1282 | except StopIteration as e: 1283 | raise ValueError("Not enough candidate gates found for locking") from e 1284 | path = [gate] 1285 | for _ in range(path_length - 1): 1286 | gate = cl.fanout(gate).pop() 1287 | path.append(gate) 1288 | paths.append(path) 1289 | 1290 | # generate banyan with J rows and I columns of SwBs 1291 | I = path_length 1292 | J = int(bw / 2) 1293 | 1294 | for i in range(I * J): 1295 | cl.add_blackbox(sbb, f"swb_{i}") 1296 | 1297 | # make connections 1298 | swb_ins = [f"swb_{i//2}.in_{i%2}" for i in range(I * J * 2)] 1299 | swb_outs = [f"swb_{i//2}.out_{i%2}" for i in range(I * J * 2)] 1300 | _connect_banyan_bb(cl, swb_ins, swb_outs, bw) 1301 | 1302 | # generate key 1303 | # In the example from the paper, the paths in a SWB directly from an 1304 | # input to an output are never used. Starting with that implemetation. 1305 | # Could sometimes choose paths less than `path_length` and use these 1306 | # connections with a decoy external input, but such a strategy is not 1307 | # discussed in the paper. 1308 | swaps = [] 1309 | key = {} 1310 | for i in range(I * J): 1311 | swaps.append(choice([True, False])) 1312 | if swaps[-1]: 1313 | key[f"swb_{i}_key_0"] = True 1314 | else: 1315 | key[f"swb_{i}_key_0"] = False 1316 | if not reduced_swb: 1317 | key[f"swb_{i}_key_1"] = False 1318 | key[f"swb_{i}_key_2"] = True 1319 | 1320 | f_gates = {} 1321 | 1322 | # Add paths to banyan 1323 | # Get a random intial ordering of paths 1324 | input_order = list(range(bw)) 1325 | shuffle(input_order) 1326 | for i, p_idx in enumerate(input_order): 1327 | path = paths[p_idx] 1328 | swb_idx = i // 2 1329 | i_idx = i % 2 1330 | prev_node = cl.fanin(path[0]).pop() 1331 | cl.connect(prev_node, f"swb_{swb_idx}.in_{i_idx}") 1332 | for j, n in enumerate(path): 1333 | o_idx = i_idx ^ int(swaps[swb_idx]) 1334 | ex_i = (cl.fanin(n) - {prev_node}).pop() 1335 | cl.connect(ex_i, f"swb_{swb_idx}.ex_in_{o_idx}") 1336 | f_gates[f"swb_{swb_idx}_f{o_idx+1}_out"] = cl.type(n) 1337 | if j != len(path) - 1: 1338 | next_n = cl.fanout(f"swb_{swb_idx}.out_{o_idx}").pop() 1339 | next_n = cl.fanout(next_n).pop() 1340 | swb_idx = int(next_n.split(".")[0].split("_")[-1]) 1341 | i_idx = int(next_n.split(".")[-1].split("_")[-1]) 1342 | prev_node = n 1343 | else: 1344 | for fo in cl.fanout(n): 1345 | cl.disconnect(n, fo) 1346 | try: 1347 | conn = cl.fanout(f"swb_{swb_idx}.out_{o_idx}").pop() 1348 | except KeyError: 1349 | conn = cl.add( 1350 | f"swb_{swb_idx}_out_{o_idx}_load", 1351 | "buf", 1352 | fanin=f"swb_{swb_idx}.out_{o_idx}", 1353 | ) 1354 | cl.connect(conn, fo) 1355 | 1356 | for path in paths: 1357 | for node in path: 1358 | cl.remove(node) 1359 | 1360 | for i in range(I * J): 1361 | cl.fill_blackbox(f"swb_{i}", s) 1362 | 1363 | for k, v in f_gates.items(): 1364 | cl.set_type(k, v) 1365 | 1366 | for k in key: 1367 | cl.set_type(k, "input") 1368 | 1369 | cg.lint(cl) 1370 | return cl, key 1371 | 1372 | 1373 | def lebl(c, bw, ng): 1374 | """Locks a circuitgraph with Logic-Enhanced Banyan Locking. 1375 | 1376 | Joseph Sweeney, Marijn J.H. Heule, and Lawrence Pileggi Modeling Techniques 1377 | for Logic Locking. In Proceedings of the International Conference on 1378 | Computer Aided Design 2020 (ICCAD-39). 1379 | 1380 | Parameters 1381 | ---------- 1382 | c: circuitgraph.CircuitGraph 1383 | Circuit to lock. 1384 | bw: int 1385 | Width of Banyan network. 1386 | lw: int 1387 | Minimum number of gates mapped to network. 1388 | 1389 | Returns 1390 | ------- 1391 | circuitgraph.CircuitGraph, dict of str:bool 1392 | the locked circuit and the correct key value for each key input 1393 | """ 1394 | from pysat.card import CardEnc 1395 | from pysat.formula import IDPool 1396 | from pysat.solvers import Cadical 1397 | 1398 | # create copy to lock 1399 | cl = c.copy() 1400 | 1401 | # generate switch and mux 1402 | s = cg.Circuit(name="switch") 1403 | m2 = cg.tx.strip_io(cg.logic.mux(2)) 1404 | s.add_subcircuit(m2, "m2_0") 1405 | s.add_subcircuit(m2, "m2_1") 1406 | m4 = cg.tx.strip_io(cg.logic.mux(4)) 1407 | s.add_subcircuit(m4, "m4_0") 1408 | s.add_subcircuit(m4, "m4_1") 1409 | s.add("in_0", "buf", fanout=["m2_0_in_0", "m2_1_in_1"]) 1410 | s.add("in_1", "buf", fanout=["m2_0_in_1", "m2_1_in_0"]) 1411 | s.add("out_0", "buf", fanin="m4_0_out") 1412 | s.add("out_1", "buf", fanin="m4_1_out") 1413 | s.add("key_0", "input", fanout=["m2_0_sel_0", "m2_1_sel_0"]) 1414 | s.add("key_1", "input", fanout=["m4_0_sel_0", "m4_1_sel_0"]) 1415 | s.add("key_2", "input", fanout=["m4_0_sel_1", "m4_1_sel_1"]) 1416 | 1417 | # generate banyan 1418 | I = int(2 * cg.utils.clog2(bw) - 2) 1419 | J = int(bw / 2) 1420 | 1421 | # add switches and muxes 1422 | for i in range(I * J): 1423 | cl.add_subcircuit(s, f"swb_{i}") 1424 | 1425 | # make connections 1426 | swb_ins = [f"swb_{i//2}_in_{i%2}" for i in range(I * J * 2)] 1427 | swb_outs = [f"swb_{i//2}_out_{i%2}" for i in range(I * J * 2)] 1428 | _connect_banyan(cl, swb_ins, swb_outs, bw) 1429 | 1430 | # get banyan io 1431 | net_ins = swb_ins[:bw] 1432 | net_outs = swb_outs[-bw:] 1433 | 1434 | # generate key 1435 | key = {f"swb_{i//3}_key_{i%3}": choice([True, False]) for i in range(3 * I * J)} 1436 | 1437 | # generate connections between banyan nodes 1438 | bfi = {n: set() for n in swb_outs + net_ins} 1439 | bfo = {n: set() for n in swb_outs + net_ins} 1440 | for n in swb_outs + net_ins: 1441 | if cl.fanout(n): 1442 | fo_node = cl.fanout(n).pop() 1443 | swb_i = fo_node.split("_")[1] 1444 | bfi[f"swb_{swb_i}_out_0"].add(n) 1445 | bfi[f"swb_{swb_i}_out_1"].add(n) 1446 | bfo[n].add(f"swb_{swb_i}_out_0") 1447 | bfo[n].add(f"swb_{swb_i}_out_1") 1448 | 1449 | # find a mapping of circuit onto banyan 1450 | net_map = IDPool() 1451 | for bn in swb_outs + net_ins: 1452 | for cn in c: 1453 | net_map.id(f"m_{bn}_{cn}") 1454 | 1455 | # mapping implications 1456 | clauses = [] 1457 | for bn in swb_outs + net_ins: 1458 | # fanin 1459 | if bfi[bn]: 1460 | for cn in c: 1461 | if c.fanin(cn): 1462 | for fcn in c.fanin(cn): 1463 | clause = [-net_map.id(f"m_{bn}_{cn}")] 1464 | clause += [net_map.id(f"m_{fbn}_{fcn}") for fbn in bfi[bn]] 1465 | clause += [net_map.id(f"m_{fbn}_{cn}") for fbn in bfi[bn]] 1466 | clauses.append(clause) 1467 | else: 1468 | clause = [-net_map.id(f"m_{bn}_{cn}")] 1469 | clause += [net_map.id(f"m_{fbn}_{cn}") for fbn in bfi[bn]] 1470 | clauses.append(clause) 1471 | 1472 | # fanout 1473 | if bfo[bn]: 1474 | for cn in c: 1475 | clause = [-net_map.id(f"m_{bn}_{cn}")] 1476 | clause += [net_map.id(f"m_{fbn}_{cn}") for fbn in bfo[bn]] 1477 | for fcn in c.fanout(cn): 1478 | clause += [net_map.id(f"m_{fbn}_{fcn}") for fbn in bfo[bn]] 1479 | clauses.append(clause) 1480 | 1481 | # no feed through 1482 | for cn in c: 1483 | net_map.id(f"INPUT_OR_{cn}") 1484 | net_map.id(f"OUTPUT_OR_{cn}") 1485 | clauses.append( 1486 | [-net_map.id(f"INPUT_OR_{cn}")] 1487 | + [net_map.id(f"m_{bn}_{cn}") for bn in net_ins] 1488 | ) 1489 | clauses.append( 1490 | [-net_map.id(f"OUTPUT_OR_{cn}")] 1491 | + [net_map.id(f"m_{bn}_{cn}") for bn in net_outs] 1492 | ) 1493 | for bn in net_ins: 1494 | clauses.append([net_map.id(f"INPUT_OR_{cn}"), -net_map.id(f"m_{bn}_{cn}")]) 1495 | for bn in net_outs: 1496 | clauses.append([net_map.id(f"OUTPUT_OR_{cn}"), -net_map.id(f"m_{bn}_{cn}")]) 1497 | clauses.append([-net_map.id(f"OUTPUT_OR_{cn}"), -net_map.id(f"INPUT_OR_{cn}")]) 1498 | 1499 | # at least ngates 1500 | for bn in swb_outs + net_ins: 1501 | net_map.id(f"NGATES_OR_{bn}") 1502 | clauses.append( 1503 | [-net_map.id(f"NGATES_OR_{bn}")] + [net_map.id(f"m_{bn}_{cn}") for cn in c] 1504 | ) 1505 | for cn in c: 1506 | clauses.append([net_map.id(f"NGATES_OR_{bn}"), -net_map.id(f"m_{bn}_{cn}")]) 1507 | clauses += CardEnc.atleast( 1508 | bound=ng, 1509 | lits=[net_map.id(f"NGATES_OR_{bn}") for bn in swb_outs + net_ins], 1510 | vpool=net_map, 1511 | ).clauses 1512 | 1513 | # at most one mapping per out 1514 | for bn in swb_outs + net_ins: 1515 | clauses += CardEnc.atmost( 1516 | lits=[net_map.id(f"m_{bn}_{cn}") for cn in c], vpool=net_map 1517 | ).clauses 1518 | 1519 | # limit number of times a gate is mapped to net outputs to fanout of gate 1520 | for cn in c: 1521 | lits = [net_map.id(f"m_{bn}_{cn}") for bn in net_outs] 1522 | bound = len(c.fanout(cn)) 1523 | if len(lits) < bound: 1524 | continue 1525 | clauses += CardEnc.atmost(bound=bound, lits=lits, vpool=net_map).clauses 1526 | 1527 | # prohibit outputs from net 1528 | for bn in swb_outs + net_ins: 1529 | for cn in c.outputs(): 1530 | clauses += [[-net_map.id(f"m_{bn}_{cn}")]] 1531 | 1532 | # solve 1533 | solver = Cadical(bootstrap_with=clauses) 1534 | if not solver.solve(): 1535 | raise ValueError(f"No config for width '{bw}'") 1536 | model = solver.get_model() 1537 | 1538 | # get mapping 1539 | mapping = {} 1540 | for bn in swb_outs + net_ins: 1541 | selected_gates = [cn for cn in c if model[net_map.id(f"m_{bn}_{cn}") - 1] > 0] 1542 | if len(selected_gates) > 1: 1543 | raise ValueError(f"Multiple gates mapped to '{bn}'") 1544 | mapping[bn] = selected_gates[0] if selected_gates else None 1545 | 1546 | potential_net_fanins = list( 1547 | c.nodes() 1548 | - (c.endpoints() | set(mapping.values()) | mapping.keys() | c.startpoints()) 1549 | ) 1550 | 1551 | # connect net inputs 1552 | for bn in net_ins: 1553 | if mapping[bn]: 1554 | cl.connect(mapping[bn], bn) 1555 | else: 1556 | cl.connect(choice(potential_net_fanins), bn) 1557 | mapping.update({cl.fanin(bn).pop(): cl.fanin(bn).pop() for bn in net_ins}) 1558 | potential_net_fanouts = list( 1559 | c.nodes() 1560 | - (c.startpoints() | set(mapping.values()) | mapping.keys() | c.endpoints()) 1561 | ) 1562 | 1563 | # connect switch boxes 1564 | for i, bn in enumerate(swb_outs): 1565 | # get keys 1566 | if key[f"swb_{i//2}_key_1"] and key[f"swb_{i//2}_key_2"]: 1567 | k = 3 1568 | elif not key[f"swb_{i//2}_key_1"] and key[f"swb_{i//2}_key_2"]: 1569 | k = 2 1570 | elif key[f"swb_{i//2}_key_1"] and not key[f"swb_{i//2}_key_2"]: 1571 | k = 1 1572 | elif not key[f"swb_{i//2}_key_1"] and not key[f"swb_{i//2}_key_2"]: 1573 | k = 0 1574 | switch_key = 1 if key[f"swb_{i//2}_key_0"] == 1 else 0 1575 | 1576 | mux_input = f"swb_{i//2}_m4_{i%2}_in_{k}" 1577 | 1578 | # connect inner nodes 1579 | mux_gate_types = set() 1580 | 1581 | # constant output, hookup to a node that is already in the affected outputs 1582 | # fanin, not in others 1583 | if not mapping[bn] and bn in net_outs: 1584 | decoy_fanout_gate = choice(potential_net_fanouts) 1585 | # selected_fo[bn] = decoy_fanout_gate 1586 | if cl.type(decoy_fanout_gate) in ["and", "nand"]: 1587 | cl.set_type(mux_input, "1") 1588 | elif cl.type(decoy_fanout_gate) in ["or", "nor", "xor", "xnor"]: 1589 | cl.set_type(mux_input, "0") 1590 | elif cl.type(decoy_fanout_gate) in ["buf"]: 1591 | if randint(0, 1): 1592 | cl.set_type(mux_input, "1") 1593 | cl.set_type(decoy_fanout_gate, choice(["and", "xnor"])) 1594 | else: 1595 | cl.set_type(mux_input, "0") 1596 | cl.set_type(decoy_fanout_gate, choice(["or", "xor"])) 1597 | elif cl.type(decoy_fanout_gate) in ["not"]: 1598 | if randint(0, 1): 1599 | cl.set_type(mux_input, "1") 1600 | cl.set_type(decoy_fanout_gate, choice(["nand", "xor"])) 1601 | else: 1602 | cl.set_type(mux_input, "0") 1603 | cl.set_type(decoy_fanout_gate, choice(["nor", "xnor"])) 1604 | elif cl.type(decoy_fanout_gate) in ["0", "1"]: 1605 | cl.set_type(mux_input, cl.type(decoy_fanout_gate)) 1606 | cl.set_type(decoy_fanout_gate, "buf") 1607 | else: 1608 | raise ValueError(f"Invalid gate type '{cl.type(decoy_fanout_gate)}'") 1609 | cl.connect(bn, decoy_fanout_gate) 1610 | mux_gate_types.add(cl.type(mux_input)) 1611 | 1612 | # feedthrough 1613 | elif mapping[bn] in [mapping[fbn] for fbn in bfi[bn]]: 1614 | cl.set_type(mux_input, "buf") 1615 | mux_gate_types.add("buf") 1616 | if mapping[cl.fanin(f"swb_{i//2}_in_0").pop()] == mapping[bn]: 1617 | cl.connect(f"swb_{i//2}_m2_{switch_key}_out", mux_input) 1618 | else: 1619 | cl.connect(f"swb_{i//2}_m2_{1-switch_key}_out", mux_input) 1620 | 1621 | # gate 1622 | elif mapping[bn]: 1623 | cl.set_type(mux_input, cl.type(mapping[bn])) 1624 | mux_gate_types.add(cl.type(mapping[bn])) 1625 | gfi = cl.fanin(mapping[bn]) 1626 | if mapping[cl.fanin(f"swb_{i//2}_in_0").pop()] in gfi: 1627 | cl.connect(f"swb_{i//2}_m2_{switch_key}_out", mux_input) 1628 | gfi.remove(mapping[cl.fanin(f"swb_{i//2}_in_0").pop()]) 1629 | if mapping[cl.fanin(f"swb_{i//2}_in_1").pop()] in gfi: 1630 | cl.connect(f"swb_{i//2}_m2_{1-switch_key}_out", mux_input) 1631 | 1632 | # mapped to None, any key works 1633 | else: 1634 | k = None 1635 | 1636 | # fill out random gates 1637 | for j in range(4): 1638 | if j != k: 1639 | t = choice( 1640 | tuple( 1641 | { 1642 | "buf", 1643 | "or", 1644 | "nor", 1645 | "and", 1646 | "nand", 1647 | "not", 1648 | "xor", 1649 | "xnor", 1650 | "0", 1651 | "1", 1652 | } 1653 | - mux_gate_types 1654 | ) 1655 | ) 1656 | mux_gate_types.add(t) 1657 | mux_input = f"swb_{i//2}_m4_{i%2}_in_{j}" 1658 | cl.set_type(mux_input, t) 1659 | if t in ("not", "buf"): 1660 | # pick a random fanin 1661 | cl.connect(f"swb_{i//2}_m2_{randint(0,1)}_out", mux_input) 1662 | elif t in ("0", "1"): 1663 | pass 1664 | else: 1665 | cl.connect(f"swb_{i//2}_m2_0_out", mux_input) 1666 | cl.connect(f"swb_{i//2}_m2_1_out", mux_input) 1667 | 1668 | # connect outputs non constant outs 1669 | rev_mapping = {} 1670 | for bn in net_outs: 1671 | if mapping[bn]: 1672 | if mapping[bn] not in rev_mapping: 1673 | rev_mapping[mapping[bn]] = set() 1674 | rev_mapping[mapping[bn]].add(bn) 1675 | 1676 | for cn, val in rev_mapping.items(): 1677 | for fcn, bn in zip_longest(cl.fanout(cn), val, fillvalue=list(val)[0]): 1678 | cl.connect(bn, fcn) 1679 | 1680 | # delete mapped gates 1681 | deleted = True 1682 | while deleted: 1683 | deleted = False 1684 | for n in cl.nodes(): 1685 | # node and all fanout are in the net 1686 | if n not in mapping and n in mapping.values(): 1687 | if all( 1688 | s not in mapping and s in mapping.values() for s in cl.fanout(n) 1689 | ): 1690 | cl.remove(n) 1691 | deleted = True 1692 | # node in net fanout 1693 | if n in [mapping[o] for o in net_outs] and n in cl: 1694 | cl.remove(n) 1695 | deleted = True 1696 | 1697 | for k in key: 1698 | cl.set_type(k, "input") 1699 | 1700 | cg.lint(cl) 1701 | return cl, key 1702 | --------------------------------------------------------------------------------