From acac41740b177a1314e11e92ec8ff09a1e6ce65f Mon Sep 17 00:00:00 2001 From: Kahsolt Date: Mon, 13 Jul 2026 16:30:14 +0800 Subject: [PATCH] =?UTF-8?q?=E3=80=90=E5=88=9B=E6=96=B0=E5=BA=94=E7=94=A8?= =?UTF-8?q?=E3=80=91=E9=87=8F=E5=AD=90=E5=AF=86=E9=92=A5=E5=88=86=E5=8F=91?= =?UTF-8?q?QKD=E5=8D=8F=E8=AE=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实现典型的五种QKD量子密钥分发协议:BB84/B92/E91/BBM92/SARG04 --- pyqpanda-algorithm/example/QKD/cmp_QKD.py | 75 +++++ pyqpanda-algorithm/example/QKD/demo_B92.py | 50 +++ pyqpanda-algorithm/example/QKD/demo_BB84.py | 51 +++ pyqpanda-algorithm/example/QKD/demo_BBM92.py | 50 +++ pyqpanda-algorithm/example/QKD/demo_E91.py | 51 +++ pyqpanda-algorithm/example/QKD/demo_SARG04.py | 51 +++ .../example/QKD/output/cmp_QKD.png | Bin 0 -> 46028 bytes pyqpanda-algorithm/pyqpanda_alg/QKD/B92.py | 66 ++++ pyqpanda-algorithm/pyqpanda_alg/QKD/BB84.py | 55 +++ pyqpanda-algorithm/pyqpanda_alg/QKD/BBM92.py | 68 ++++ pyqpanda-algorithm/pyqpanda_alg/QKD/E91.py | 111 ++++++ pyqpanda-algorithm/pyqpanda_alg/QKD/QKD.py | 315 ++++++++++++++++++ pyqpanda-algorithm/pyqpanda_alg/QKD/SARG04.py | 74 ++++ .../pyqpanda_alg/QKD/__init__.py | 39 +++ pyqpanda-algorithm/pyqpanda_alg/__init__.py | 1 + test/QKD/Test_B92.py | 44 +++ test/QKD/Test_BB84.py | 56 ++++ test/QKD/Test_BBM92.py | 44 +++ test/QKD/Test_E91.py | 52 +++ test/QKD/Test_QKD.py | 72 ++++ test/QKD/Test_SARG04.py | 44 +++ test/pytest.ini | 1 + 22 files changed, 1370 insertions(+) create mode 100644 pyqpanda-algorithm/example/QKD/cmp_QKD.py create mode 100644 pyqpanda-algorithm/example/QKD/demo_B92.py create mode 100644 pyqpanda-algorithm/example/QKD/demo_BB84.py create mode 100644 pyqpanda-algorithm/example/QKD/demo_BBM92.py create mode 100644 pyqpanda-algorithm/example/QKD/demo_E91.py create mode 100644 pyqpanda-algorithm/example/QKD/demo_SARG04.py create mode 100644 pyqpanda-algorithm/example/QKD/output/cmp_QKD.png create mode 100644 pyqpanda-algorithm/pyqpanda_alg/QKD/B92.py create mode 100644 pyqpanda-algorithm/pyqpanda_alg/QKD/BB84.py create mode 100644 pyqpanda-algorithm/pyqpanda_alg/QKD/BBM92.py create mode 100644 pyqpanda-algorithm/pyqpanda_alg/QKD/E91.py create mode 100644 pyqpanda-algorithm/pyqpanda_alg/QKD/QKD.py create mode 100644 pyqpanda-algorithm/pyqpanda_alg/QKD/SARG04.py create mode 100644 pyqpanda-algorithm/pyqpanda_alg/QKD/__init__.py create mode 100644 test/QKD/Test_B92.py create mode 100644 test/QKD/Test_BB84.py create mode 100644 test/QKD/Test_BBM92.py create mode 100644 test/QKD/Test_E91.py create mode 100644 test/QKD/Test_QKD.py create mode 100644 test/QKD/Test_SARG04.py diff --git a/pyqpanda-algorithm/example/QKD/cmp_QKD.py b/pyqpanda-algorithm/example/QKD/cmp_QKD.py new file mode 100644 index 0000000..0b08f01 --- /dev/null +++ b/pyqpanda-algorithm/example/QKD/cmp_QKD.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Compare the five QKD protocols side by side. + +Runs BB84, B92, E91, BBM92 and SARG04 over a clean channel with a low/high-intensity +intercept-resend eavesdropper, then reports sift efficiency and the quantum +bit-error rate (QBER) for each. In every case the QBER stays at 0 without an +eavesdropper and rises well above the ~11 % security threshold once Eve is on the +line -- the signature that lets the parties detect her. + +Outputs (written to ./output): + cmp_QKD.png -- sift-efficiency and QBER bar charts + +Run: + python cmp_QKD.py +""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[2])) +from pyqpanda_alg.QKD import QKD, BB84, B92, E91, BBM92, SARG04 + +import matplotlib ; matplotlib.use('Agg') +import matplotlib.pyplot as plt + +BASE_PATH = Path(__file__).resolve().parent +OUT_DIR = BASE_PATH / 'output' + +PROTOCOLS: list[QKD] = [BB84, B92, E91, BBM92, SARG04] +N_RAW = 30000 +THRESHOLD = 0.11 +EVE_LOW_PROB = 0.2 +EVE_HIGH_PROB = 0.5 + + +if __name__ == '__main__': + names, sift, qber_low, qber_high = [], [], [], [] + print(f'{"protocol":8s} {"sift":>7s} {"QBER(Low)":>12s} {"QBER(High)":>10s} secure(Low/High)') + print('-' * 60) + for P in PROTOCOLS: + eve_low = P(eve_prob=EVE_LOW_PROB, seed=1).distribute(N_RAW) + eve_high = P(eve_prob=EVE_HIGH_PROB, seed=1).distribute(N_RAW) + names.append(P.__name__) + sift.append(eve_low.sift_rate) + qber_low.append(eve_low.qber) + qber_high.append(eve_high.qber) + chsh = '' + if 'chsh_S' in eve_low.extra: + chsh = f" CHSH S: Low={eve_low.extra['chsh_S']:+.2f} High={eve_high.extra['chsh_S']:+.2f}" + print(f'{P.__name__:8s} {eve_low.sift_rate:6.1%} {eve_low.qber:12.3f} {eve_high.qber:10.3f} {eve_low.secure}/{eve_high.secure}{chsh}') + + x = range(len(names)) + fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(11, 4)) + ax1.bar(x, sift, color='#4c72b0') + ax1.set_xticks(list(x)) ; ax1.set_xticklabels(names, rotation=20) + ax1.set_ylabel('sift efficiency') + ax1.set_title('Sift efficiency (kept fraction)') + ax1.set_ylim(0, 0.6) + for i, v in zip(x, sift): + ax1.text(i, v + 0.01, f'{v:.0%}', ha='center', va='bottom', fontsize=9) + w = 0.38 + ax2.bar([i - w / 2 for i in x], qber_low, width=w, label=f'Low intercepts {EVE_LOW_PROB:.0%}', color='#55a868') + ax2.bar([i + w / 2 for i in x], qber_high, width=w, label=f'High intercepts {EVE_HIGH_PROB:.0%}', color='#c44e52') + ax2.axhline(THRESHOLD, ls='--', color='k', lw=1, label=f'security threshold {THRESHOLD:.0%}') + ax2.set_xticks(list(x)) ; ax2.set_xticklabels(names, rotation=20) + ax2.set_ylabel('QBER') + ax2.set_title('QBER: eavesdropped low vs high intensity') + ax2.legend(fontsize=8) + fig.tight_layout() + OUT_DIR.mkdir(exist_ok=True) + path = OUT_DIR / 'cmp_QKD.png' + fig.savefig(path, dpi=120, bbox_inches='tight') + print(f'\nSaved comparison figure to {path}') diff --git a/pyqpanda-algorithm/example/QKD/demo_B92.py b/pyqpanda-algorithm/example/QKD/demo_B92.py new file mode 100644 index 0000000..5e189f3 --- /dev/null +++ b/pyqpanda-algorithm/example/QKD/demo_B92.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Demo: B92 quantum key distribution. + +B92 needs only two non-orthogonal states: Alice sends |0> for bit 0 and |+> for +bit 1. Bob measures in a random basis and keeps only the *conclusive* outcomes +(about 25 % of rounds) that unambiguously identify Alice's bit. As with BB84 an +eavesdropper is exposed by the resulting error rate. + +Run: + python demo_B92.py +""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[2])) +from pyqpanda_alg.QKD import B92, QKDInsecureError + + +def report(title: str, qkd: B92, n_raw: int): + r = qkd.distribute(n_raw) + print(f'\n[{title}]') + print(f' raw qubits : {r.n_raw}') + print(f' conclusive (sifted) : {len(r.alice_key)} ({r.sift_rate:.1%})') + print(f' QBER : {r.qber:.3f} (secure={r.secure})') + print(f' Alice sifted key : {r.alice_key[:48]}...') + print(f' Bob sifted key : {r.bob_key[:48]}...') + print(f' keys identical : {r.alice_key == r.bob_key}') + print(f' timecost : {r.timecost:.3f}s') + + +if __name__ == '__main__': + print('=' * 64) + print('B92 -- prepare-and-measure QKD (2 non-orthogonal states)') + print('=' * 64) + + report('clean channel', B92(seed=42), 8192) + report('Eve intercepts every round', B92(eve_prob=1.0, seed=42), 8192) + + print('\n[keygen over a clean channel]') + key = B92(seed=7).keygen(128) + print(f' 128-bit privacy-amplified key: {key}') + + print('\n[keygen with full interception -> aborts]') + try: + B92(eve_prob=1.0, seed=7).keygen(128) + except QKDInsecureError as e: + print(f' {e}') diff --git a/pyqpanda-algorithm/example/QKD/demo_BB84.py b/pyqpanda-algorithm/example/QKD/demo_BB84.py new file mode 100644 index 0000000..2d97ea7 --- /dev/null +++ b/pyqpanda-algorithm/example/QKD/demo_BB84.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Demo: BB84 quantum key distribution. + +Alice sends random bits in random bases (Z/X); Bob measures in his own random +bases; they sift the rounds where the bases matched. The demo runs the protocol +over a clean channel (Alice and Bob obtain an identical key with QBER 0) and then +with an intercept-resend eavesdropper (QBER jumps to ~25 % and the key is +rejected). + +Run: + python demo_BB84.py +""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[2])) +from pyqpanda_alg.QKD import BB84, QKDInsecureError + + +def report(title: str, qkd: BB84, n_raw: int): + r = qkd.distribute(n_raw) + print(f'\n[{title}]') + print(f' raw qubits : {r.n_raw}') + print(f' sifted (bases match) : {len(r.alice_key)} ({r.sift_rate:.1%})') + print(f' QBER : {r.qber:.3f} (secure={r.secure})') + print(f' Alice sifted key : {r.alice_key[:48]}...') + print(f' Bob sifted key : {r.bob_key[:48]}...') + print(f' keys identical : {r.alice_key == r.bob_key}') + print(f' timecost : {r.timecost:.3f}s') + + +if __name__ == '__main__': + print('=' * 64) + print('BB84 -- prepare-and-measure QKD (4 states, 2 bases)') + print('=' * 64) + + report('clean channel', BB84(seed=42), 8192) + report('Eve intercepts every round', BB84(eve_prob=1.0, seed=42), 8192) + + print('\n[keygen over a clean channel]') + key = BB84(seed=7).keygen(128) + print(f' 128-bit privacy-amplified key: {key}') + + print('\n[keygen with full interception -> aborts]') + try: + BB84(eve_prob=1.0, seed=7).keygen(128) + except QKDInsecureError as e: + print(f' {e}') diff --git a/pyqpanda-algorithm/example/QKD/demo_BBM92.py b/pyqpanda-algorithm/example/QKD/demo_BBM92.py new file mode 100644 index 0000000..50e48e1 --- /dev/null +++ b/pyqpanda-algorithm/example/QKD/demo_BBM92.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Demo: BBM92 entanglement-based quantum key distribution. + +BBM92 is the EPR version of BB84: instead of Alice preparing states, a source +emits Bell pairs |Phi+> and both parties measure in a random Z/X basis. Because +|Phi+> is perfectly correlated in both bases, matching-basis rounds yield the key. +Intercepting one arm breaks the correlation and shows up as QBER, just like BB84. + +Run: + python demo_BBM92.py +""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[2])) +from pyqpanda_alg.QKD import BBM92, QKDInsecureError + + +def report(title: str, qkd: BBM92, n_raw: int): + r = qkd.distribute(n_raw) + print(f'\n[{title}]') + print(f' entangled pairs : {r.n_raw}') + print(f' sifted (bases match) : {len(r.alice_key)} ({r.sift_rate:.1%})') + print(f' QBER : {r.qber:.3f} (secure={r.secure})') + print(f' Alice sifted key : {r.alice_key[:48]}...') + print(f' Bob sifted key : {r.bob_key[:48]}...') + print(f' keys identical : {r.alice_key == r.bob_key}') + print(f' timecost : {r.timecost:.3f}s') + + +if __name__ == '__main__': + print('=' * 64) + print('BBM92 -- entanglement-based twin of BB84 (EPR pairs)') + print('=' * 64) + + report('clean channel', BBM92(seed=42), 8192) + report('Eve intercepts every round', BBM92(eve_prob=1.0, seed=42), 8192) + + print('\n[keygen over a clean channel]') + key = BBM92(seed=7).keygen(128) + print(f' 128-bit privacy-amplified key: {key}') + + print('\n[keygen with full interception -> aborts]') + try: + BBM92(eve_prob=1.0, seed=7).keygen(128) + except QKDInsecureError as e: + print(f' {e}') diff --git a/pyqpanda-algorithm/example/QKD/demo_E91.py b/pyqpanda-algorithm/example/QKD/demo_E91.py new file mode 100644 index 0000000..57dc441 --- /dev/null +++ b/pyqpanda-algorithm/example/QKD/demo_E91.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Demo: E91 (Ekert) entanglement-based quantum key distribution. + +A source shares Bell pairs. Alice and Bob each measure along random settings; +rounds with the same physical angle give perfectly correlated bits (the key), +while the mismatched-angle rounds are spent on the CHSH test. Genuine +entanglement yields S ~ 2*sqrt(2) = 2.83; an eavesdropper collapses the +entanglement, pushing S back toward the classical bound of 2 and raising the QBER. + +Run: + python demo_E91.py +""" + +import sys +import math +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[2])) +from pyqpanda_alg.QKD import E91, QKDInsecureError + + +def report(title: str, qkd: E91, n_raw: int): + r = qkd.distribute(n_raw) + print(f'\n[{title}]') + print(f' entangled pairs : {r.n_raw}') + print(f' key rounds (sifted) : {len(r.alice_key)} ({r.sift_rate:.1%})') + print(f' QBER : {r.qber:.3f} (secure={r.secure})') + print(f" CHSH S : {r.extra['chsh_S']:+.3f} (|S|>2 => entangled; ideal {2*math.sqrt(2):.3f})") + print(f' keys identical : {r.alice_key == r.bob_key}') + print(f' timecost : {r.timecost:.3f}s') + + +if __name__ == '__main__': + print('=' * 64) + print('E91 -- entanglement-based QKD with a CHSH Bell test') + print('=' * 64) + + report('clean channel', E91(seed=42), 8192) + report('Eve intercepts every round', E91(eve_prob=1.0, seed=42), 8192) + + print('\n[keygen over a clean channel]') + key = E91(seed=7).keygen(128) + print(f' 128-bit privacy-amplified key: {key}') + + print('\n[keygen with full interception -> aborts]') + try: + E91(eve_prob=1.0, seed=7).keygen(128) + except QKDInsecureError as e: + print(f' {e}') diff --git a/pyqpanda-algorithm/example/QKD/demo_SARG04.py b/pyqpanda-algorithm/example/QKD/demo_SARG04.py new file mode 100644 index 0000000..b50664d --- /dev/null +++ b/pyqpanda-algorithm/example/QKD/demo_SARG04.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Demo: SARG04 quantum key distribution. + +SARG04 uses the same four states as BB84 but encodes the key in the *basis* and +sifts differently: Alice announces the state she sent together with a decoy from +the conjugate basis, and Bob keeps a round only when his measurement excludes one +of the two. This makes it more robust against photon-number-splitting attacks. +An intercept-resend eavesdropper still drives up the QBER. + +Run: + python demo_SARG04.py +""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[2])) +from pyqpanda_alg.QKD import SARG04, QKDInsecureError + + +def report(title: str, qkd: SARG04, n_raw: int): + r = qkd.distribute(n_raw) + print(f'\n[{title}]') + print(f' raw qubits : {r.n_raw}') + print(f' conclusive (sifted) : {len(r.alice_key)} ({r.sift_rate:.1%})') + print(f' QBER : {r.qber:.3f} (secure={r.secure})') + print(f' Alice sifted key : {r.alice_key[:48]}...') + print(f' Bob sifted key : {r.bob_key[:48]}...') + print(f' keys identical : {r.alice_key == r.bob_key}') + print(f' timecost : {r.timecost:.3f}s') + + +if __name__ == '__main__': + print('=' * 64) + print('SARG04 -- BB84 states, basis-encoded key, exclusion sifting') + print('=' * 64) + + report('clean channel', SARG04(seed=42), 8192) + report('Eve intercepts every round', SARG04(eve_prob=1.0, seed=42), 8192) + + print('\n[keygen over a clean channel]') + key = SARG04(seed=7).keygen(128) + print(f' 128-bit privacy-amplified key: {key}') + + print('\n[keygen with full interception -> aborts]') + try: + SARG04(eve_prob=1.0, seed=7).keygen(128) + except QKDInsecureError as e: + print(f' {e}') diff --git a/pyqpanda-algorithm/example/QKD/output/cmp_QKD.png b/pyqpanda-algorithm/example/QKD/output/cmp_QKD.png new file mode 100644 index 0000000000000000000000000000000000000000..f59a81f69b7e55e7c38a21dc0acd64d0194492b0 GIT binary patch literal 46028 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 literal 0 HcmV?d00001 diff --git a/pyqpanda-algorithm/pyqpanda_alg/QKD/B92.py b/pyqpanda-algorithm/pyqpanda_alg/QKD/B92.py new file mode 100644 index 0000000..b177c25 --- /dev/null +++ b/pyqpanda-algorithm/pyqpanda_alg/QKD/B92.py @@ -0,0 +1,66 @@ +""" +B92 (Bennett, 1992) -- a minimalist prepare-and-measure protocol that needs only +two non-orthogonal states. + +Alice encodes bit 0 as |0> and bit 1 as |+>. Bob measures each qubit in a random +basis (Z or X) and keeps only *conclusive* outcomes: + + * measured in Z and got |1> => the state cannot have been |0>, so Alice sent + |+> => key bit 1; + * measured in X and got |-> => the state cannot have been |+>, so Alice sent + |0> => key bit 0. + +All other outcomes are inconclusive and discarded, giving a ~25 % sift rate in the +ideal case. Because the two states are non-orthogonal, no measurement can +distinguish them deterministically -- which is exactly what keeps Eve out. + +~ref: https://en.wikipedia.org/wiki/B92_protocol +~ref: http://home.ustc.edu.cn/~gongsiqiu/_book/6-Quantum%20communication/Quantum%20Cryptography%20Using%20Any%20Two%20Nonorthogonal%20States.html +""" + +from .QKD import QKD, Z_BASIS, X_BASIS + + +class B92(QKD): + + '''BB84的简化版,双基双态;通过基不一致反推''' + + sift_efficiency = 0.25 + + def _exchange(self, n_raw: int) -> dict: + eve = self._eve_mask(n_raw) + a_bits = self._randbits(n_raw) # KEY BITS + b_basis = self._randbits(n_raw) + e_basis = self._randbits(n_raw) if any(eve) else None + + bob = n_raw * [-1] + keep = n_raw * [False] + for i in range(n_raw): + # 1) Alice选值v,依规则使用对应的基B,制备并发送量子态 |phi> = B|0> + # v=0: 用Z基制备 |phi> = I|0> = |0> + # v=1: 用X基制备 |phi> = H|0> = |+> + prep_basis = X_BASIS if a_bits[i] == 1 else Z_BASIS + prep_bit = 0 # 始终是0 + phi = self._basis_prepare(prep_basis, prep_bit) + # 2) Eve拦截,选基测量 |phi> 并重放测量结果 + if eve[i]: + intercept_basis = e_basis[i] + eve_bit = self._basis_measure(phi, intercept_basis) + phi = self._basis_prepare(intercept_basis, eve_bit) + # 3) Bob选基并测量 |phi> + out = self._basis_measure(phi, b_basis[i]) + # 4) 若测量值为1则保留(只可能是制备基-测量基不同导致),当前比特是否被保留 + if out == 1: # 与prep_bit相反 + if b_basis[i] == Z_BASIS: + bob[i], keep[i] = 1, True # Bob用Z基测出了1,说明Alice用的X基制备的v=1 + elif b_basis[i] == X_BASIS: + bob[i], keep[i] = 0, True # Bob用X基测出了1,说明Alice用的Z基制备的v=0 + + return { + 'alice': a_bits, + 'bob': bob, + 'keep': keep, + 'extra': { + 'eve_intercepts': eve, + } + } diff --git a/pyqpanda-algorithm/pyqpanda_alg/QKD/BB84.py b/pyqpanda-algorithm/pyqpanda_alg/QKD/BB84.py new file mode 100644 index 0000000..e03a013 --- /dev/null +++ b/pyqpanda-algorithm/pyqpanda_alg/QKD/BB84.py @@ -0,0 +1,55 @@ +""" +BB84 (Bennett & Brassard, 1984) -- the original prepare-and-measure protocol. + +For every qubit Alice draws a random bit and a random basis (Z: |0>,|1> or +X: |+>,|->), prepares the corresponding state and sends it. Bob measures in his +own random basis. During sifting they keep only the rounds where their bases +match; on those the two bits are identical in an ideal channel. An +intercept-resend eavesdropper, forced to guess the basis, corrupts 25 % of the +sifted bits and is exposed by the QBER. + +~ref: https://en.wikipedia.org/wiki/BB84 +~ref: https://arxiv.org/abs/2003.06557 +""" + +from .QKD import QKD + + +class BB84(QKD): + + '''最基础的非纠缠QKD协议,双基四态''' + + sift_efficiency = 0.5 + + def _exchange(self, n_raw: int) -> dict: + eve = self._eve_mask(n_raw) + a_bits = self._randbits(n_raw) # KEY BITS + a_basis = self._randbits(n_raw) + b_basis = self._randbits(n_raw) + e_basis = self._randbits(n_raw) if any(eve) else None + + bob = n_raw * [-1] + keep = n_raw * [False] + for i in range(n_raw): + # 1) Alice选值v和基B,制备并发送量子态 |phi> = B|v> + prep_basis, prep_bit = a_basis[i], a_bits[i] + phi = self._basis_prepare(prep_basis, prep_bit) + # 2) Eve拦截,选基测量 |phi> 并重放测量结果 + if eve[i]: + intercept_basis = e_basis[i] + eve_bit = self._basis_measure(phi, intercept_basis) + phi = self._basis_prepare(intercept_basis, eve_bit) + # 3) Bob选基并测量 |phi> + out = self._basis_measure(phi, b_basis[i]) + # 4) Alice和Bob互相告知自己所选的基,若同基则保留测量值 + if a_basis[i] == b_basis[i]: + bob[i], keep[i] = out, True + + return { + 'alice': a_bits, + 'bob': bob, + 'keep': keep, + 'extra': { + 'eve_intercepts': eve, + } + } diff --git a/pyqpanda-algorithm/pyqpanda_alg/QKD/BBM92.py b/pyqpanda-algorithm/pyqpanda_alg/QKD/BBM92.py new file mode 100644 index 0000000..8f76c6d --- /dev/null +++ b/pyqpanda-algorithm/pyqpanda_alg/QKD/BBM92.py @@ -0,0 +1,68 @@ +""" +BBM92 (Bennett, Brassard & Mermin, 1992) -- the entanglement-based twin of BB84. + +Instead of Alice preparing states, a shared source emits Bell pairs +|Phi+> = (|00>+|11>)/sqrt2. Alice and Bob each measure their qubit in a random +basis (Z or X). Since |Phi+> is perfectly correlated in *both* bases +( = (|++>+|-->)/sqrt2 ), whenever their bases agree their outcomes are identical, +yielding the raw key. An eavesdropper who intercepts and resends one arm breaks +the entanglement and shows up as errors, exactly as in BB84. + +~ref: https://en.wikipedia.org/wiki/BBM92_protocol +~ref: https://www.sci-hub.in/10.1103/physrevlett.68.557 +""" + +from .QKD import QKD, Z_BASIS, X_BASIS, PI + +# Measuring "at angle theta" == apply RY(-theta) then measure Z; the X basis is +# theta = pi/2 (RY(-pi/2) sends |+>->|0>, |->->|1>, matching a Hadamard). +# Z -> 0, X -> pi/2 +ROT_ANGLES = { + Z_BASIS: 0, + X_BASIS: PI / 2, +} + + +class BBM92(QKD): + + '''最基础的纠缠QKD协议,BB84的纠缠版''' + + sift_efficiency = 0.5 + + def _exchange(self, n_raw: int) -> dict: + eve = self._eve_mask(n_raw) + a_basis = self._randbits(n_raw) + b_basis = self._randbits(n_raw) + e_basis = self._randbits(n_raw) if any(eve) else None + + alice = n_raw * [-1] + bob = n_raw * [-1] + keep = n_raw * [False] + for i in range(n_raw): + # 1) Charlie 分发一个 |Φ+> 纠缠对给Alice和Bob + phi = self._bell_prepare() + # 2)Alice和Bob各自选基、旋转并测量 + a_rot, b_rot = ROT_ANGLES[a_basis[i]], ROT_ANGLES[b_basis[i]] + if eve[i]: + # Eve在Bob拦截,选基旋转、测量并重放结果给Bob + # Bob以为自己操作的是纠缠对中的一侧,实际是Eve制备的一个新的单比特系统 + e_rot = ROT_ANGLES[e_basis[i]] + self._bell_rot(phi, a_rot, e_rot) + a_out, e_out = self._bell_measure(phi) + b_out = self._resend(e_rot, e_out, b_rot) + else: + self._bell_rot(phi, a_rot, b_rot) + a_out, b_out = self._bell_measure(phi) + alice[i], bob[i] = a_out, b_out + # 3) Alice和Bob互相告知自己所选的基,若同基则保留测量值 + if a_basis[i] == b_basis[i]: + keep[i] = True + + return { + 'alice': alice, + 'bob': bob, + 'keep': keep, + 'extra': { + 'eve_intercepts': eve, + } + } diff --git a/pyqpanda-algorithm/pyqpanda_alg/QKD/E91.py b/pyqpanda-algorithm/pyqpanda_alg/QKD/E91.py new file mode 100644 index 0000000..c83d106 --- /dev/null +++ b/pyqpanda-algorithm/pyqpanda_alg/QKD/E91.py @@ -0,0 +1,111 @@ +""" +E91 (Ekert, 1991) -- entanglement-based QKD secured by a Bell inequality. + +A source shares Bell pairs |Phi+> = (|00>+|11>)/sqrt2. Alice measures her qubit +along one of three angles {0, pi/4, pi/2}; Bob along {pi/4, pi/2, 3pi/4}. + + * Key: rounds where they happen to pick the *same physical angle* + (Alice pi/4 = Bob pi/4, and Alice pi/2 = Bob pi/2) are perfectly + correlated and become the shared key. + * Security: the mismatched-angle rounds feed the CHSH quantity + S = E(a1,b1) - E(a1,b3) + E(a3,b1) + E(a3,b3). + Genuine entanglement gives |S| = 2*sqrt2 ~ 2.83; any + intercept-resend eavesdropper drives S back toward the classical + bound of 2 (and simultaneously raises the QBER). + +~ref: https://en.wikipedia.org/wiki/Quantum_key_distribution#E91_protocol:_Artur_Ekert_.281991.29 +~ref: https://cqi.inf.usi.ch/qic/91_Ekert.pdf +""" + +from .QKD import QKD, PI + +# Alice's three settings +A_ANGLES = { + 0: 0.0, + 1: PI / 4, + 2: PI / 2, +} +# Bob's three settings +B_ANGLES = { + 0: PI / 4, + 1: PI / 2, + 2: 3 * PI / 4, +} +assert len(A_ANGLES) == len(B_ANGLES) +# Eve guesses from the union +E_ANGLES = sorted(set(A_ANGLES.values()) | set(B_ANGLES.values())) +# valid rounds whose physical angles coincide -> raw key: (a_idx, b_idx) +# can configure by yourself according to A_ANGLES and B_ANGLES +_KEY_SETTINGS = { + (1, 0), # PI / 4 + (2, 1), # PI / 2 +} +# CHSH uses Alice {0, pi/2} = idx {0, 2} and Bob {pi/4, 3pi/4} = idx {0, 2} +# https://en.wikipedia.org/wiki/CHSH_inequality +_CHSH_SIGN = { + (0, 0): +1, + (0, 2): -1, + (2, 0): +1, + (2, 2): +1, +} + + +class E91(QKD): + + '''BBM92的三态版本;使用CHSH检测窃听''' + + sift_efficiency = len(_KEY_SETTINGS) / len(A_ANGLES)**2 + + def _exchange(self, n_raw: int) -> dict: + eve = self._eve_mask(n_raw) + a_idx = self._randtrits(n_raw) + b_idx = self._randtrits(n_raw) + e_ang = self._rng.choices(E_ANGLES, k=n_raw) if any(eve) else None + + alice = n_raw * [-1] + bob = n_raw * [-1] + keep = n_raw * [False] + + # accumulate correlations for CHSH: sum of s_A*s_B and counts per setting + corr_sum = {s: 0.0 for s in _CHSH_SIGN} + corr_cnt = {s: 0 for s in _CHSH_SIGN} + for i in range(n_raw): + # 1) Charlie 分发一个 |Φ+> 纠缠对给Alice和Bob + phi = self._bell_prepare() + # 2)Alice和Bob各自选角度、旋转并测量 + ai, bi = a_idx[i], b_idx[i] + a_rot, b_rot = A_ANGLES[ai], B_ANGLES[bi] + if eve[i]: + # Eve在Bob拦截,选基旋转、测量并重放结果给Bob + # Bob以为自己操作的是纠缠对中的一侧,实际是Eve制备的一个新的单比特系统 + e_rot = e_ang[i] + self._bell_rot(phi, a_rot, e_rot) + a_out, e_out = self._bell_measure(phi) + b_out = self._resend(e_rot, e_out, b_rot) + else: + self._bell_rot(phi, a_rot, b_rot) + a_out, b_out = self._bell_measure(phi) + alice[i], bob[i] = a_out, b_out + # 3) Alice和Bob互相告知自己所选的旋转角度,若一致则保留测量值 + if (ai, bi) in _KEY_SETTINGS: + keep[i] = True + # 4) 计算CHSH quantity,验证纠缠保持度 + if (ai, bi) in corr_sum: + s = (-1) ** (a_out == b_out) + corr_sum[(ai, bi)] += s # homo=+1, heter=-1 + corr_cnt[(ai, bi)] += 1 + + S = 0.0 + for s in _CHSH_SIGN: + E = corr_sum[s] / corr_cnt[s] if corr_cnt[s] else 0.0 + S += _CHSH_SIGN[s] * E + + return { + 'alice': alice, + 'bob': bob, + 'keep': keep, + 'extra': { + 'eve_intercepts': eve, + 'chsh_S': S, + } + } diff --git a/pyqpanda-algorithm/pyqpanda_alg/QKD/QKD.py b/pyqpanda-algorithm/pyqpanda_alg/QKD/QKD.py new file mode 100644 index 0000000..86d0932 --- /dev/null +++ b/pyqpanda-algorithm/pyqpanda_alg/QKD/QKD.py @@ -0,0 +1,315 @@ +""" +Common infrastructure shared by every quantum-key-distribution protocol. + +A QKD protocol lets two parties (Alice and Bob) grow a shared secret key whose +secrecy is guaranteed by quantum mechanics rather than by computational +hardness: the no-cloning theorem and the measurement-disturbance principle mean +that any eavesdropper (Eve) unavoidably imprints detectable errors on the raw +key. Every protocol here follows the same three stages + + 1. quantum transmission -- Alice encodes random bits into qubits which Bob + measures (implemented with real ``pyqpanda3`` + circuits, one circuit per transmitted qubit); + 2. sifting -- over a public classical channel the two parties + keep only the rounds that are physically + correlated (e.g. matching bases); + 3. parameter estimation -- a random subset of the sifted bits is disclosed + to estimate the quantum bit-error rate (QBER); a + QBER above the protocol threshold reveals Eve and + the key is discarded; + 4. privacy amplification -- the undisclosed candidate key is compressed with + a random binary Toeplitz hash, removing partial + information that Eve may have learned. + +Sub-classes only implement :meth:`_exchange`, which performs stages 1-2 for a +block of raw qubits and returns Alice's/Bob's raw bits plus the sift mask. The +base class turns that into a :class:`QKDResult` and exposes :meth:`keygen`. + +~ref: https://en.wikipedia.org/wiki/Quantum_key_distribution +""" + +import math +import random +from time import perf_counter +from dataclasses import dataclass, field + +from pyqpanda3.core import CPUQVM, QProg, H, X, RY, CNOT, measure + +# measurement / preparation bases +Z_BASIS = 0 # computational basis |0>, |1> +X_BASIS = 1 # Hadamard basis |+>, |-> + +PI = math.pi + +mean = lambda x: sum(x) / len(x) if x else 0.0 + + +def _bits_to_str(bits: list[int]) -> str: + return ''.join('1' if b else '0' for b in bits) + + +class QKDInsecureError(RuntimeError): + """Raised by :meth:`QKD.keygen` when the estimated QBER exceeds the security + threshold, i.e. an eavesdropper is suspected and the key must be thrown away.""" + + +@dataclass +class QKDResult: + """Outcome of a single key-distribution round (a block of ``n_raw`` qubits).""" + + protocol: str + n_raw: int + alice_key: str # Alice's sifted bits (before public disclosure), as '0'/'1' + bob_key: str # Bob's sifted bits + key: str # candidate key after disclosure, before privacy amplification + sift_rate: float # kept fraction = len(sifted) / n_raw + qber: float # estimated quantum bit-error rate on the disclosed sample + secure: bool # qber <= threshold + timecost: float # time cost + extra: dict = field(default_factory=dict) + + def __repr__(self) -> str: + return (f'QKDResult({self.protocol}, n_raw={self.n_raw}, ' + f'sift_rate={self.sift_rate:.2f}, qber={self.qber:.3f}, ' + f'secure={self.secure}, key_len={len(self.key)})') + + +class QKD: + """Base class: quantum transmission helpers, sifting, QBER estimation, keygen. + + Args: + eve_prob: probability that Eve intercepts and resends each transmitted + qubit. ``0`` disables Eve and ``1`` attacks every round. + sample_fraction: fraction of the sifted key publicly disclosed to estimate + the QBER (those bits are removed from the final key). + min_sample_size: minimum disclosed sample when ``sample_fraction`` is + non-zero and enough sifted bits exist. A useful lower bound keeps + short-key QBER estimates from being dominated by sampling noise. + qber_threshold: QBER above which the key is deemed insecure (BB84's + asymptotic bound is ~11 %). + privacy_ratio: final-key length divided by the candidate-key length used + for Toeplitz privacy amplification. The default 0.5 hashes two + candidate bits into one final bit. + seed: seed for the (classical) random-number generator, for reproducibility. + """ + + #: rough kept-fraction after sifting, used only to size the raw block in keygen + sift_efficiency = 0.5 + + def __init__(self, + eve_prob: float = 0.0, + sample_fraction: float = 0.2, + qber_threshold: float = 0.11, + privacy_ratio: float = 0.5, + min_sample_size: int = 128, + seed: int = None, + ): + assert 0.0 <= eve_prob <= 1.0, 'eve_prob must be in [0, 1]' + assert 0.0 <= sample_fraction < 1.0, 'sample_fraction must be in [0, 1)' + assert 0.0 <= qber_threshold <= 1.0, 'qber_threshold must be in [0, 1]' + assert 0.0 < privacy_ratio <= 1.0, 'privacy_ratio must be in (0, 1]' + assert min_sample_size >= 1, 'min_sample_size must be >= 1' + self.eve_prob = float(eve_prob) + self.sample_fraction = float(sample_fraction) + self.qber_threshold = float(qber_threshold) + self.privacy_ratio = float(privacy_ratio) + self.min_sample_size = int(min_sample_size) + self._rng = random.Random(seed) + self._qvm = CPUQVM() + + @property + def name(self) -> str: + return self.__class__.__name__ + + # ------------------------------------------------------------------ # + # low-level quantum helpers (one circuit == one transmitted qubit) # + # ------------------------------------------------------------------ # + def _run(self, prog: QProg, n_cbits: int) -> list[int]: + """Run ``prog`` for a single shot and return the measured bits as a list + indexed by classical-bit number (``cbit j`` at list position ``j``).""" + self._qvm.run(prog, 1) + bitstr = next(iter(self._qvm.result().get_counts())) # big-endian over cbits + return [int(bitstr[n_cbits - 1 - j]) for j in range(n_cbits)] + + def _basis_prepare(self, prep_basis: int, prep_bit: int) -> QProg: + """Prepare primitive: prepare one qubit as ``prep_bit`` in ``prep_basis`` (Z/X)""" + prog = QProg(1) + if prep_bit: + prog << X(0) # 0/1 -> |0>/|1> + if prep_basis == X_BASIS: + prog << H(0) # |0>/|1> -> |+>/|-> + return prog + + def _basis_measure(self, prog: QProg, meas_basis: int) -> int: + """Measure primitive: measure a qstate in ``meas_basis`` (Z/X), return the outcome.""" + if meas_basis == X_BASIS: + prog << H(0) # rotate X basis back to computational for read-out + prog << measure(0, 0) + return self._run(prog, 1)[0] + + def _bell_prepare(self) -> QProg: + """Create a Bell pair |Phi+> = (|00>+|11>)/sqrt2.""" + prog = QProg(2) + prog << H(0) << CNOT(0, 1) + return prog + + def _bell_rot(self, prog: QProg, rot0: float, rot1: float) -> QProg: + """Rot the qubits at ``rot`` (RY angles) in a bell-state pair""" + prog << RY(0, -rot0) << RY(1, -rot1) + return prog + + def _bell_measure(self, prog: QProg) -> list[int, int]: + """Measure a bell-state pair, returns (out0, out1). + For |Phi+> the two spin outcomes correlate as `` = cos(rot0-rot1)``, + so equal rotations give identical bits -- the raw key of the entanglement + protocols.""" + prog << measure(0, 0) << measure(1, 1) + return self._run(prog, 2) + + def _resend(self, prep_rot: float, prep_bit: int, meas_rot: float) -> int: + """Prepare the eigenstate that Eve collapsed onto (``prep_bit`` at angle + ``prep_rot``) and measure it at ``meas_rot``. Used to model intercept-resend + on an entangled arm.""" + prog = QProg(1) + if prep_bit: + prog << X(0) + prog << RY(0, prep_rot) # rebuild the measured eigenstate + prog << RY(0, -meas_rot) # receiver's measurement rotation + prog << measure(0, 0) + return self._run(prog, 1)[0] + + def _randbits(self, n_len: int) -> list[int]: + return [self._rng.randrange(0, 2) for _ in range(n_len)] + + def _randtrits(self, n_len: int) -> list[int]: + return [self._rng.randrange(0, 3) for _ in range(n_len)] + + def _eve_mask(self, n_raw: int) -> list[bool]: + """Draw the rounds attacked by an intercept-resend Eve. + + The exact endpoints avoid consuming random numbers when Eve is entirely + absent or present, while intermediate probabilities make an independent + Bernoulli decision for every transmitted qubit. + """ + if self.eve_prob <= 0.0: return [False] * n_raw + if self.eve_prob >= 1.0: return [True] * n_raw + return [self._rng.random() < self.eve_prob for _ in range(n_raw)] + + # ------------------------------------------------------------------ # + # protocol hook + public API # + # ------------------------------------------------------------------ # + def _exchange(self, n_raw: int) -> dict: + """Run quantum transmission + sifting for ``n_raw`` raw qubits. + + Sub-classes must return a dict with keys ``alice`` (int array of Alice's + raw bits), ``bob`` (Bob's raw bits), ``keep`` (bool sift mask), and an + optional ``extra`` dict of protocol-specific diagnostics.""" + raise NotImplementedError + + def distribute(self, n_raw: int) -> QKDResult: + """Distribute a block of ``n_raw`` raw qubits and return a :class:`QKDResult`.""" + assert n_raw >= 1, 'n_raw must be >= 1' + + ts_start = perf_counter() + data = self._exchange(n_raw) + ts_end = perf_counter() + + alice: list[int] = data['alice'] + bob: list[int] = data['bob'] + keep: list[bool] = data['keep'] + a_sift = [alice[i] for i, k in enumerate(keep) if k] + b_sift = [bob [i] for i, k in enumerate(keep) if k] + n_sift = len(a_sift) + + # publicly disclose a random sample to estimate the QBER + n_sample = round(self.sample_fraction * n_sift) + if self.sample_fraction > 0.0 and n_sift > 1: + n_sample = max(n_sample, min(self.min_sample_size, n_sift - 1)) + if self.eve_prob > 0.0 and n_sift > 0: + n_sample = max(1, n_sample) + if n_sift > 1: + n_sample = min(n_sample, n_sift - 1) # never disclose the whole key + + if n_sample > 0: + idx = self._rng.sample(range(n_sift), k=n_sample) + a_sift_sample = [a_sift[i] for i in idx] + b_sift_sample = [b_sift[i] for i in idx] + qber = mean([a != b for a, b in zip(a_sift_sample, b_sift_sample)]) + key_bits = [a_sift[i] for i in range(n_sift) if i not in idx] + else: + qber = mean([a != b for a, b in zip(a_sift, b_sift)]) if n_sift else 0.0 + key_bits = a_sift + + return QKDResult( + protocol=self.name, + n_raw=n_raw, + alice_key=_bits_to_str(a_sift), + bob_key=_bits_to_str(b_sift), + key=_bits_to_str(key_bits), + sift_rate=n_sift / n_raw, + qber=qber, + secure=qber <= self.qber_threshold, + timecost=ts_end - ts_start, + extra=data.get('extra', {}), + ) + + def privacy_amplification(self, key_bits: str | list[int], final_len: int) -> list[int]: + """Hash ``n`` candidate bits to ``final_len`` bits with a Toeplitz matrix. + + A binary ``m x n`` Toeplitz matrix is fully described by ``m + n - 1`` + random bits (one per diagonal). Multiplication is performed over GF(2). + This is a universal-hash privacy-amplification step; ``keygen`` controls + the compression ratio through :attr:`privacy_ratio`. + """ + if isinstance(key_bits, str): + assert set(key_bits) <= {'0', '1'}, 'key_bits must be binary' + bits = [1 if ch == '1' else 0 for ch in key_bits] + else: + assert isinstance(key_bits, list) + bits = key_bits + assert isinstance(final_len, int) and final_len >= 1, 'final_len must be >= 1' + assert final_len <= len(bits), f'privacy amplification needs at least {final_len} candidate bits' + + n = len(bits) + diagonals = self._randbits(n + final_len - 1) + out = [0] * final_len + for i in range(final_len): + start = final_len - 1 - i + row = diagonals[start:start + n] + res = [a & b for a, b in zip(bits, row)] + z = res[0] + for zz in res[1:]: + z ^= zz + out[i] = z + return out + + def keygen(self, n_len: int) -> str: + """Generate a shared secret key of ``n_len`` bits (returned as a '0'/'1' string). + + Raw qubits are transmitted in blocks until enough candidate bits accumulate. + If an eavesdropper is detected (QBER over threshold) a + :class:`QKDInsecureError` is raised and the key is discarded. The candidate + key is then compressed to exactly ``n_len`` bits by Toeplitz hashing.""" + assert isinstance(n_len, int) and n_len >= 1, 'n_len must be >= 1' + + # Privacy amplification consumes a longer candidate key and hashes it to + # the requested final length. + candidate_len = math.ceil(n_len / self.privacy_ratio) + candidate = '' + while len(candidate) < candidate_len: + # Size every block (including top-ups) to cover both proportional + # disclosure and the minimum parameter-estimation sample. + needed = candidate_len - len(candidate) + proportional_need = needed / max(1.0 - self.sample_fraction, 1e-3) + minimum_need = needed + (self.min_sample_size if self.sample_fraction > 0.0 else 0) + sift_needed = max(proportional_need, minimum_need) + block = math.ceil(sift_needed / max(self.sift_efficiency, 1e-3)) + 32 + res = self.distribute(block) + if not res.secure: + raise QKDInsecureError( + f'{self.name}: estimated QBER {res.qber:.3f} exceeds threshold ' + f'{self.qber_threshold:.3f} -- eavesdropping suspected, key discarded') + candidate += res.key + + final = self.privacy_amplification(candidate[:candidate_len], n_len) + return _bits_to_str(final) diff --git a/pyqpanda-algorithm/pyqpanda_alg/QKD/SARG04.py b/pyqpanda-algorithm/pyqpanda_alg/QKD/SARG04.py new file mode 100644 index 0000000..559414c --- /dev/null +++ b/pyqpanda-algorithm/pyqpanda_alg/QKD/SARG04.py @@ -0,0 +1,74 @@ +""" +SARG04 (Scarani, Acin, Ribordy & Gisin, 2004) -- same four states as BB84 but a +different sifting rule that hardens it against photon-number-splitting attacks. + +Alice sends one of the four BB84 states; the *key bit is the basis she used* +(0 = Z, 1 = X), not the state value. During sifting she does not reveal the +basis; instead she announces an unordered pair made of the state she sent and one +decoy from the conjugate basis. Bob, who measured in a random basis, can identify +the sent state only when his outcome is orthogonal to -- and therefore excludes -- +the decoy: + + conclusive <=> Bob measured in the conjugate basis AND his outcome is the + opposite value of the decoy. + +His own outcome can never be orthogonal to the true state, so the exclusion is +unambiguous and he recovers Alice's basis (the key bit). The ideal sift rate is +~25 %. + +~ref: https://en.wikipedia.org/wiki/SARG04 +~ref: https://arxiv.org/abs/quant-ph/0211131 +""" + +from .QKD import QKD + + +class SARG04(QKD): + + '''BB84+B92缝合,双基四态;通过基不一致反推''' + + sift_efficiency = 0.25 + + def _exchange(self, n_raw: int) -> dict: + eve = self._eve_mask(n_raw) + a_basis = self._randbits(n_raw) # Alice's basis = KEY BIT + a_bits = self._randbits(n_raw) # Alice's prepare bits + d_bits = self._randbits(n_raw) # Alice's decoy bits (assumed from the conjugate basis) + b_basis = self._randbits(n_raw) + e_basis = self._randbits(n_raw) if any(eve) else None + + bob = n_raw * [-1] + keep = n_raw * [False] + for i in range(n_raw): + # 1) Alice选值v和基B,制备并发送量子态 |phi> = B|v> + prep_basis, prep_bit = a_basis[i], a_bits[i] + phi = self._basis_prepare(prep_basis, prep_bit) + # 2) Eve拦截,选基测量 |phi> 并重放测量结果 + if eve[i]: + intercept_basis = e_basis[i] + eve_bit = self._basis_measure(phi, intercept_basis) + phi = self._basis_prepare(intercept_basis, eve_bit) + # 3) Bob选基并测量 |phi> + out = self._basis_measure(phi, b_basis[i]) + # 4) Alice告知Bob刚才制备的 |phi> 来自于集合 {|0>/|1>, |+>/|->} + # 其中一个是正确答案,另一个是相反基的诱骗答案 (故Alice没有直接公布基) + # Bob通过基不一致反推检查自己的测量结果,并告知Alice当前比特是否被保留 + # 若Alice宣告 {|0>, |+>}, 则 Bob 必须测出 1(Z基) 或 -(X基) 才能确定值为v=0 + # 若Alice宣告 {|0>, |->}, 则 Bob 必须测出 1(Z基) 或 +(X基) 才能确定值为v=1/0 + # 若Alice宣告 {|1>, |+>}, 则 Bob 必须测出 0(Z基) 或 -(X基) 才能确定值为v=0/1 + # 若Alice宣告 {|1>, |->}, 则 Bob 必须测出 0(Z基) 或 +(X基) 才能确定值为v=1 + mismatch_sent = b_basis[i] == a_basis[i] and out != a_bits[i] + mismatch_decoy = b_basis[i] == 1 - a_basis[i] and out != d_bits[i] + if mismatch_decoy and not mismatch_sent: + bob[i], keep[i] = a_basis[i], True + elif mismatch_sent and not mismatch_decoy: + bob[i], keep[i] = 1 - a_basis[i], True + + return { + 'alice': a_basis, + 'bob': bob, + 'keep': keep, + 'extra': { + 'eve_intercepts': eve, + } + } diff --git a/pyqpanda-algorithm/pyqpanda_alg/QKD/__init__.py b/pyqpanda-algorithm/pyqpanda_alg/QKD/__init__.py new file mode 100644 index 0000000..2aee4be --- /dev/null +++ b/pyqpanda-algorithm/pyqpanda_alg/QKD/__init__.py @@ -0,0 +1,39 @@ +''' +Quantum key distribution (QKD) is a secure communication method that implements +a cryptographic protocol based on the laws of quantum mechanics, specifically +quantum entanglement, measurement-disturbance principle, and no-cloning theorem. +~ref: https://en.wikipedia.org/wiki/Quantum_key_distribution + +Five classic protocols are provided, all built on real ``pyqpanda3`` circuits and +sharing the same sift / QBER-estimation pipeline: + + BB84 prepare-and-measure, four states in two bases + B92 prepare-and-measure, two non-orthogonal states + E91 entanglement-based, secured by the CHSH Bell inequality + BBM92 entanglement-based twin of BB84 (EPR pairs) + SARG04 four BB84 states, basis-encoded key, exclusion-based sifting + +Example: + >>> from pyqpanda_alg.QKD import BB84 + >>> key = BB84(seed=42).keygen(16) + >>> len(key) + 16 +''' + +from .QKD import QKD, QKDResult, QKDInsecureError +from .B92 import B92 +from .BB84 import BB84 +from .BBM92 import BBM92 +from .E91 import E91 +from .SARG04 import SARG04 + +__all__ = [ + 'QKD', + 'QKDResult', + 'QKDInsecureError', + 'B92', + 'BB84', + 'BBM92', + 'E91', + 'SARG04', +] diff --git a/pyqpanda-algorithm/pyqpanda_alg/__init__.py b/pyqpanda-algorithm/pyqpanda_alg/__init__.py index 12d6808..b592c7d 100644 --- a/pyqpanda-algorithm/pyqpanda_alg/__init__.py +++ b/pyqpanda-algorithm/pyqpanda_alg/__init__.py @@ -51,4 +51,5 @@ from . import Grover from . import QmRMR from . import QSEncode +from . import QKD diff --git a/test/QKD/Test_B92.py b/test/QKD/Test_B92.py new file mode 100644 index 0000000..8de262a --- /dev/null +++ b/test/QKD/Test_B92.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Tests for pyqpanda_alg.QKD.B92 (two-state prepare-and-measure QKD).""" + +import sys +from pathlib import Path + +import pytest + +sys.path.insert(0, str(Path(__file__).resolve().parents[2] / 'pyqpanda-algorithm')) +from pyqpanda_alg.QKD import B92, QKDInsecureError + + +class TestB92: + """B92 量子密钥分发测试""" + + def test_keygen_length_and_alphabet(self): + key = B92(seed=1).keygen(64) + assert isinstance(key, str) + assert len(key) == 64 + assert set(key) <= {'0', '1'} + + def test_clean_channel_is_error_free(self): + r = B92(seed=2).distribute(3000) + assert r.qber == 0.0 + assert r.secure is True + assert r.alice_key == r.bob_key + + def test_sift_efficiency_about_quarter(self): + r = B92(seed=3).distribute(4000) + assert abs(r.sift_rate - 0.25) < 0.05 # only conclusive outcomes kept + + def test_eve_probability_raises_qber(self): + r = B92(eve_prob=1.0, seed=4).distribute(4000) + assert r.qber > 0.12 + assert r.secure is False + + def test_keygen_aborts_under_full_interception(self): + with pytest.raises(QKDInsecureError): + B92(eve_prob=1.0, seed=5).keygen(64) + + +if __name__ == '__main__': + pytest.main([__file__, '-v', '-s']) diff --git a/test/QKD/Test_BB84.py b/test/QKD/Test_BB84.py new file mode 100644 index 0000000..155c936 --- /dev/null +++ b/test/QKD/Test_BB84.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Tests for pyqpanda_alg.QKD.BB84 (prepare-and-measure quantum key distribution).""" + +import sys +from pathlib import Path + +import pytest + +sys.path.insert(0, str(Path(__file__).resolve().parents[2] / 'pyqpanda-algorithm')) +from pyqpanda_alg.QKD import BB84, QKDInsecureError, mean + + +class TestBB84: + """BB84 量子密钥分发测试""" + + def test_keygen_length_and_alphabet(self): + key = BB84(seed=1).keygen(64) + assert isinstance(key, str) + assert len(key) == 64 + assert set(key) <= {'0', '1'} + + def test_clean_channel_is_error_free(self): + r = BB84(seed=2).distribute(2000) + assert r.qber == 0.0 + assert r.secure is True + assert r.alice_key == r.bob_key # Alice and Bob share the key + + def test_sift_efficiency_about_half(self): + r = BB84(seed=3).distribute(4000) + assert abs(r.sift_rate - 0.5) < 0.05 # bases agree ~half the time + + def test_eve_probability_raises_qber(self): + r = BB84(eve_prob=1.0, seed=4).distribute(4000) + assert r.qber > 0.12 # intercept-resend -> ~25 % + assert r.secure is False + assert all(r.extra['eve_intercepts']) + + def test_keygen_aborts_under_full_interception(self): + with pytest.raises(QKDInsecureError): + BB84(eve_prob=1.0, seed=5).keygen(64) + + def test_partial_eve_probability_is_applied_per_round(self): + r = BB84(eve_prob=0.5, seed=10).distribute(6000) + assert abs(mean(r.extra['eve_intercepts']) - 0.5) < 0.03 + assert 0.07 < r.qber < 0.18 # expected QBER ~= eve_prob / 4 + + def test_bb84_bits_are_reproducible(self): + # clean-channel sifted key is fixed by the classical choices -> reproducible + k1 = BB84(seed=9).keygen(48) + k2 = BB84(seed=9).keygen(48) + assert k1 == k2 + + +if __name__ == '__main__': + pytest.main([__file__, '-v', '-s']) diff --git a/test/QKD/Test_BBM92.py b/test/QKD/Test_BBM92.py new file mode 100644 index 0000000..ee5fba5 --- /dev/null +++ b/test/QKD/Test_BBM92.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Tests for pyqpanda_alg.QKD.BBM92 (EPR-based twin of BB84).""" + +import sys +from pathlib import Path + +import pytest + +sys.path.insert(0, str(Path(__file__).resolve().parents[2] / 'pyqpanda-algorithm')) +from pyqpanda_alg.QKD import BBM92, QKDInsecureError + + +class TestBBM92: + """BBM92 纠缠式量子密钥分发测试""" + + def test_keygen_length_and_alphabet(self): + key = BBM92(seed=1).keygen(64) + assert isinstance(key, str) + assert len(key) == 64 + assert set(key) <= {'0', '1'} + + def test_clean_channel_is_error_free(self): + r = BBM92(seed=2).distribute(2000) + assert r.qber == 0.0 + assert r.secure is True + assert r.alice_key == r.bob_key # EPR pairs give both parties the same bit + + def test_sift_efficiency_about_half(self): + r = BBM92(seed=3).distribute(4000) + assert abs(r.sift_rate - 0.5) < 0.05 + + def test_eve_probability_raises_qber(self): + r = BBM92(eve_prob=1.0, seed=4).distribute(4000) + assert r.qber > 0.12 + assert r.secure is False + + def test_keygen_aborts_under_full_interception(self): + with pytest.raises(QKDInsecureError): + BBM92(eve_prob=1.0, seed=5).keygen(64) + + +if __name__ == '__main__': + pytest.main([__file__, '-v', '-s']) diff --git a/test/QKD/Test_E91.py b/test/QKD/Test_E91.py new file mode 100644 index 0000000..f0d0d68 --- /dev/null +++ b/test/QKD/Test_E91.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Tests for pyqpanda_alg.QKD.E91 (entanglement-based QKD with a CHSH test).""" + +import sys +import math +from pathlib import Path + +import pytest + +sys.path.insert(0, str(Path(__file__).resolve().parents[2] / 'pyqpanda-algorithm')) +from pyqpanda_alg.QKD import E91, QKDInsecureError + + +class TestE91: + """E91 纠缠式量子密钥分发测试""" + + def test_keygen_length_and_alphabet(self): + key = E91(seed=1).keygen(48) + assert isinstance(key, str) + assert len(key) == 48 + assert set(key) <= {'0', '1'} + + def test_clean_channel_is_error_free(self): + r = E91(seed=2).distribute(6000) + assert r.qber == 0.0 + assert r.secure is True + assert r.alice_key == r.bob_key + + def test_sift_efficiency_two_ninths(self): + r = E91(seed=3).distribute(6000) + assert abs(r.sift_rate - 2.0 / 9.0) < 0.05 + + def test_chsh_violation_without_eve(self): + r = E91(seed=3).distribute(6000) + # genuine entanglement violates the classical bound of 2 (ideal 2*sqrt2) + assert r.extra['chsh_S'] > 2.4 + assert r.extra['chsh_S'] <= 2 * math.sqrt(2) + 0.15 # within statistics of Tsirelson + + def test_eve_probability_breaks_entanglement(self): + r = E91(eve_prob=1.0, seed=4).distribute(6000) + assert r.qber > 0.12 + assert r.secure is False + assert r.extra['chsh_S'] < 2.0 # CHSH falls back below the classical bound + + def test_keygen_aborts_under_full_interception(self): + with pytest.raises(QKDInsecureError): + E91(eve_prob=1.0, seed=5).keygen(48) + + +if __name__ == '__main__': + pytest.main([__file__, '-v', '-s']) diff --git a/test/QKD/Test_QKD.py b/test/QKD/Test_QKD.py new file mode 100644 index 0000000..4934eab --- /dev/null +++ b/test/QKD/Test_QKD.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Tests for shared QKD post-processing and probabilistic Eve controls.""" + +import sys +import random +from pathlib import Path + +import pytest + +sys.path.insert(0, str(Path(__file__).resolve().parents[2] / 'pyqpanda-algorithm')) +from pyqpanda_alg.QKD import BB84, QKD + + +class TestQKDBase: + + @pytest.mark.parametrize('eve_prob', [-0.01, 1.01]) + def test_invalid_eve_probability(self, eve_prob): + with pytest.raises(AssertionError, match='eve_prob'): + BB84(eve_prob=eve_prob) + + @pytest.mark.parametrize('privacy_ratio', [0.0, -0.1, 1.01]) + def test_invalid_privacy_ratio(self, privacy_ratio): + with pytest.raises(AssertionError, match='privacy_ratio'): + BB84(privacy_ratio=privacy_ratio) + + def test_toeplitz_privacy_amplification(self): + bits = [0, 1, 1, 0, 1, 0, 0, 1] + final_len = 4 + seed = 17 + + # Reproduce the same random Toeplitz diagonals independently and perform + # the GF(2) matrix-vector product explicitly. + rng = random.Random(seed) + diagonals = [rng.randrange(0, 2) for _ in range(len(bits) + final_len - 1)] + expected = [0] * final_len + for i in range(final_len): + start = final_len - 1 - i + diag = diagonals[start:start + len(bits)] + res = [a & b for a, b in zip(bits, diag)] + z = res[0] + for zz in res[1:]: + z ^= zz + expected[i] = z + + actual = QKD(seed=seed)._privacy_amplification(bits, final_len) + assert all(a == b for a, b in zip(actual, expected)) + + def test_keygen_hashes_longer_candidate_key(self): + qkd = BB84(privacy_ratio=0.5, seed=23) + call = {} + + def privacy_spy(key_bits, final_len): + call['candidate_len'] = len(key_bits) + call['final_len'] = final_len + return [0] * final_len + + qkd._privacy_amplification = privacy_spy + key = qkd.keygen(24) + + assert key == '0' * 24 + assert call == {'candidate_len': 48, 'final_len': 24} + + def test_privacy_amplification_is_seeded_for_reproducibility(self): + bits = [0, 1, 1, 0] * 32 + out1 = QKD(seed=99)._privacy_amplification(bits, 48) + out2 = QKD(seed=99)._privacy_amplification(bits, 48) + assert all(a == b for a, b in zip(out1, out2)) + + +if __name__ == '__main__': + pytest.main([__file__, '-v', '-s']) diff --git a/test/QKD/Test_SARG04.py b/test/QKD/Test_SARG04.py new file mode 100644 index 0000000..e2eeed4 --- /dev/null +++ b/test/QKD/Test_SARG04.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Tests for pyqpanda_alg.QKD.SARG04 (basis-encoded prepare-and-measure QKD).""" + +import sys +from pathlib import Path + +import pytest + +sys.path.insert(0, str(Path(__file__).resolve().parents[2] / 'pyqpanda-algorithm')) +from pyqpanda_alg.QKD import SARG04, QKDInsecureError + + +class TestSARG04: + """SARG04 量子密钥分发测试""" + + def test_keygen_length_and_alphabet(self): + key = SARG04(seed=1).keygen(64) + assert isinstance(key, str) + assert len(key) == 64 + assert set(key) <= {'0', '1'} + + def test_clean_channel_is_error_free(self): + r = SARG04(seed=2).distribute(3000) + assert r.qber == 0.0 + assert r.secure is True + assert r.alice_key == r.bob_key + + def test_sift_efficiency_about_quarter(self): + r = SARG04(seed=3).distribute(4000) + assert abs(r.sift_rate - 0.25) < 0.05 # exclusion sifting keeps ~1/4 + + def test_eve_probability_raises_qber(self): + r = SARG04(eve_prob=1.0, seed=4).distribute(4000) + assert r.qber > 0.12 + assert r.secure is False + + def test_keygen_aborts_under_full_interception(self): + with pytest.raises(QKDInsecureError): + SARG04(eve_prob=1.0, seed=5).keygen(64) + + +if __name__ == '__main__': + pytest.main([__file__, '-v', '-s']) diff --git a/test/pytest.ini b/test/pytest.ini index 2887f64..5f5f574 100644 --- a/test/pytest.ini +++ b/test/pytest.ini @@ -5,6 +5,7 @@ testpaths = QRAM QPCA QSVM + QKD python_files =