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 0000000..f59a81f Binary files /dev/null and b/pyqpanda-algorithm/example/QKD/output/cmp_QKD.png differ 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 =