1. はじめに
アリスとボブが、安全でない通信路(例えばインターネット)上で秘密のメッセージ(例えばボブのオンラインバンクの詳細)をやりとりしたい場合、メッセージを暗号化することが必要です。暗号は広い分野で、そのほとんどはこのテキストの範囲外なので、アリスとボブが他の誰にも知られていない秘密鍵を持っていればそれが役に立ち、共通鍵暗号で通信できる、ということを信頼するほかはありません。
アリスとボブがイブの古典的通信路を使って鍵を共有したい場合、イブが自分用にこの鍵のコピーを作ったかどうかを知ることは不可能です。つまり、盗聴されていないということについて、イブを全面的に信頼するしかありません。ところが、もしイブが量子的通信路を提供しているのであれば、アリスとボブはもはやイブを信頼する必要はありません。つまり、ボブのメッセージがアリスに届く前にイブがそれを読もうとすれば、彼らにはそのことがわかるのです。
量子的通信路が物理的にどのように実装されるのか、そのイメージをお伝えするとわかりやすい方もいるかもしれません。古典的通信路は例えば電話線に例えることができます。電話線を通じて、電気信号であらわされたメッセージ(ないしビット列)を送ります。提案されている量子的通信路の例としては、例えばある種の光ファイバーケーブルを挙げることができるでしょう。これを通じて1個ずつの光子(光の粒子)を送ることができます。光子には 偏光 と呼ばれる特性があり、この偏光は2つの状態のうちのいずれかを取ることができます。これを使って、量子ビットを表現することができます。
2. プロトコルの概要
このプロトコルは、量子ビットを測定するとその量子ビットの状態が変化しうる、という事実を利用しています。アリスがボブに量子ビットを送り、ボブが測定する前に盗聴者(イブ)が測定を試みると、イブの測定によって量子ビットの状態が変化し、アリスが送信した量子ビットの状態がボブに届かない可能性があります。
from qiskit import QuantumCircuit, Aer, transpile, assemble
from qiskit.visualization import plot_histogram, plot_bloch_multivector
from numpy.random import randint
import numpy as np
print("Imports Successful")
アリスが状態 $|+\rangle$ (X基底で 0
) の量子ビットを準備し、ボブがそれをX基底で測定すれば、ボブは必ず 0
を得ます:
qc = QuantumCircuit(1,1)
# Alice prepares qubit in state |+>
qc.h(0)
qc.barrier()
# Alice now sends the qubit to Bob
# who measures it in the X-basis
qc.h(0)
qc.measure(0,0)
# Draw and simulate circuit
display(qc.draw())
aer_sim = Aer.get_backend('aer_simulator')
job = aer_sim.run(assemble(qc))
plot_histogram(job.result().get_counts())
しかしこの量子ビットがボブに届く前にイブがZ基底での測定を試みると、量子ビットの状態を $|+\rangle$ から $|0\rangle$ または $|1\rangle$ のどちらかに変えることになり、ボブが測定して0を得ることはもはや確実ではなくなります:
qc = QuantumCircuit(1,1)
# Alice prepares qubit in state |+>
qc.h(0)
# Alice now sends the qubit to Bob
# but Eve intercepts and tries to read it
qc.measure(0, 0)
qc.barrier()
# Eve then passes this on to Bob
# who measures it in the X-basis
qc.h(0)
qc.measure(0,0)
# Draw and simulate circuit
display(qc.draw())
aer_sim = Aer.get_backend('aer_simulator')
job = aer_sim.run(assemble(qc))
plot_histogram(job.result().get_counts())
このように、ボブは50%の確率で 1
を測定する可能性があることがわかります。そしてもし 1
を測定すれば、彼とアリスは自分たちが使っている通信路に何か問題があるということがわかります。
量子鍵配送プロトコルは、このプロセスをじゅうぶんな回数繰り返して、盗聴者がこの迎撃から逃れる可能性がほとんどなくなるようにする方法です。おおまかには次のようになります:
- ステップ1
アリスはランダムなビット列を選びます。たとえば:
1000101011010100
そして、各ビットに対応するランダムな基底を選びます:
ZZXZXXXZXZXXXXXX
アリスは、この2種類の情報を自分だけの秘密として持っておきます。
- ステップ2
次にアリスは、各ビットを自分が選んだ基底を使って量子ビット列上にエンコードします。そうすると、各量子ビットは、ランダムに選ばれた $|0\rangle$, $|1\rangle$, $|+\rangle$, $|-\rangle$ のいずれかの状態になります。この場合、量子ビット列は次のようになるでしょう:
$$ |1\rangle|0\rangle|+\rangle|0\rangle|-\rangle|+\rangle|-\rangle|0\rangle|-\rangle|1\rangle|+\rangle|-\rangle|+\rangle|-\rangle|+\rangle|+\rangle $$これが、彼女がボブに送るメッセージになります。
- ステップ3
次にボブは、それぞれの量子ビットをランダムに測定します。たとえば次のような基底を使うかもしれません:
XZZZXZXZXZXZZZXZ
そしてボブはその測定結果を秘密にしておきます。
- ステップ4
そうしてからボブとアリスは、各量子ビットに対して自分たちが使った基底を公開しあいます。アリスがある量子ビットを用意したときに使った基底と同じ基底を使って、ボブがその量子ビットを測定していた場合には、彼らが共有する秘密鍵の一部としてそのビットを使用することにします。そうでなかった場合には、そのビットについての情報は捨てます。
- ステップ5
最後に、ボブとアリスは彼らの鍵のなかからランダムなサンプルを共有します。そのサンプルが一致していれば、(誤りの余地が少しばかりありますが)伝送が成功したと信じることができます。
擬似ランダム鍵を生成するには、numpyの randint
関数を使います。このページに示したのと同じ結果が再現していただけるよう、種には 0 を設定しておきます:
np.random.seed(seed=0)
アリスの最初のメッセージの長さを n
と呼ぶことにします。この例では、アリスは100量子ビットの長さのメッセージを送ります:
n = 100
np.random.seed(seed=0)
n = 100
## Step 1
# Alice generates bits
alice_bits = randint(2, size=n)
print(alice_bits)
この時点では、ビット列 'alice_bits
' はアリスしか知りません。アリスだけが知っている情報は何か、ボブだけが知っている情報は何か、イブの通信路を流れたのは何か、をこのような表で追跡することにします:
Alice's Knowledge | Over Eve's Channel | Bob's Knowledge |
---|---|---|
alice_bits |
3.2 ステップ2:
アリスは各ビットを、ランダムに選んだ $X$ または $Z$基底を使って量子ビット上にエンコードします。選んだ基底は alice_bases
に保持します。ここでは、 0
は「$Z$基底を使った」、1
は「$X$基底を使った」ことを意味します。
np.random.seed(seed=0)
n = 100
## Step 1
#Alice generates bits
alice_bits = randint(2, size=n)
## Step 2
# Create an array to tell us which qubits
# are encoded in which bases
alice_bases = randint(2, size=n)
print(alice_bases)
アリスはこれも秘密にしておきます:
Alice's Knowledge | Over Eve's Channel | Bob's Knowledge |
---|---|---|
alice_bits | ||
alice_bases |
下記の encode_message
関数は QuantumCircuit
のリストを生成します。リストの各要素が、アリスのメッセージに含まれる個々の量子ビットを表しています:
def encode_message(bits, bases):
message = []
for i in range(n):
qc = QuantumCircuit(1,1)
if bases[i] == 0: # Prepare qubit in Z-basis
if bits[i] == 0:
pass
else:
qc.x(0)
else: # Prepare qubit in X-basis
if bits[i] == 0:
qc.h(0)
else:
qc.x(0)
qc.h(0)
qc.barrier()
message.append(qc)
return message
np.random.seed(seed=0)
n = 100
## Step 1
# Alice generates bits
alice_bits = randint(2, size=n)
## Step 2
# Create an array to tell us which qubits
# are encoded in which bases
alice_bases = randint(2, size=n)
message = encode_message(alice_bits, alice_bases)
alices_bits
に含まれる最初のビットが 0
であり、アリスがこれをエンコードした基底は $X$基底( 1
で表される)である、ということがわかります:
print('bit = %i' % alice_bits[0])
print('basis = %i' % alice_bases[0])
そして message
に含まれる最初の回路(アリスのメッセージに含まれる最初の量子ビットを表している)を見ると、アリスが状態 $|+\rangle$ の量子ビットを用意したことを確かめることができます:
message[0].draw()
別の例として、 alice_bits
の4番目のビットは 1
であり、 $Z$基底でエンコードされていることがわかります。アリスは対応する量子ビットを $|1\rangle$ の状態で準備しているわけです:
print('bit = %i' % alice_bits[4])
print('basis = %i' % alice_bases[4])
message[4].draw()
np.random.seed(seed=0)
n = 100
## Step 1
# Alice generates bits
alice_bits = randint(2, size=n)
## Step 2
# Create an array to tell us which qubits
# are encoded in which bases
alice_bases = randint(2, size=n)
message = encode_message(alice_bits, alice_bases)
## Step 3
# Decide which basis to measure in:
bob_bases = randint(2, size=n)
print(bob_bases)
bob_bases
には、ボブが各量子ビットを測定するのに選んだ基底が保持されます。
Alice's Knowledge | Over Eve's Channel | Bob's Knowledge |
---|---|---|
alice_bits | ||
alice_bases | ||
message | message | message |
下記の関数 measure_message
が対応する測定に使われ、各量子ビットを測定した結果をシミュレートします。測定した結果は bob_results
に保持します。
def measure_message(message, bases):
backend = Aer.get_backend('aer_simulator')
measurements = []
for q in range(n):
if bases[q] == 0: # measuring in Z-basis
message[q].measure(0,0)
if bases[q] == 1: # measuring in X-basis
message[q].h(0)
message[q].measure(0,0)
aer_sim = Aer.get_backend('aer_simulator')
qobj = assemble(message[q], shots=1, memory=True)
result = aer_sim.run(qobj).result()
measured_bit = int(result.get_memory()[0])
measurements.append(measured_bit)
return measurements
np.random.seed(seed=0)
n = 100
## Step 1
# Alice generates bits
alice_bits = randint(2, size=n)
## Step 2
# Create an array to tell us which qubits
# are encoded in which bases
alice_bases = randint(2, size=n)
message = encode_message(alice_bits, alice_bases)
## Step 3
# Decide which basis to measure in:
bob_bases = randint(2, size=n)
bob_results = measure_message(message, bob_bases)
message[0]
(ゼロ番目の量子ビットを表している)の回路が、ボブによって $X$測定を施されたことがわかります:
message[0].draw()
ボブはアリスがエンコードしたときと同じ基底をたまたま選んで測定したので、ボブは 0
という結果を得ることが保証されています。下に示すように6番目の量子ビットでは、ボブがランダムに選んだ測定はアリスが選んだものとは同じでないため、ボブの結果がアリスのものと一致する可能性は50%しかありません。
message[6].draw()
print(bob_results)
ボブは自分の結果を秘密にしておきます。
Alice's Knowledge | Over Eve's Channel | Bob's Knowledge |
---|---|---|
alice_bits | ||
alice_bases | ||
message | message | message |
bob_bases | ||
bob_results |
3.4 ステップ4:
この後、アリスはどの量子ビットがどの基底でエンコードされたかを(イブの通信路を使って)公開します:
Alice's Knowledge | Over Eve's Channel | Bob's Knowledge |
---|---|---|
alice_bits | ||
alice_bases | ||
message | message | message |
bob_bases | ||
bob_results | ||
alice_bases | alice_bases |
そしてボブはそれぞれの量子ビットをどの基底で測定したかを公開します:
Alice's Knowledge | Over Eve's Channel | Bob's Knowledge |
---|---|---|
alice_bits | ||
alice_bases | ||
message | message | message |
bob_bases | ||
bob_results | ||
alice_bases | alice_bases | |
bob_bases | bob_bases |
ボブがたまたまあるビットを、アリスが用意したときと同じ基底で測定していれば、 bob_results
中のその箇所は alice_bits
中の対応する箇所と一致し、このビットを彼らの鍵の一部として使うことができます。2人が別々の基底で測定していれば、ボブの結果はランダムになるので、両者ともその箇所は捨てます。remove_garbage
はこれを行うための関数です:
def remove_garbage(a_bases, b_bases, bits):
good_bits = []
for q in range(n):
if a_bases[q] == b_bases[q]:
# If both used the same basis, add
# this to the list of 'good' bits
good_bits.append(bits[q])
return good_bits
アリスとボブは使えないビットを捨て、残ったビットを使って彼らの秘密鍵を作ります:
np.random.seed(seed=0)
n = 100
## Step 1
# Alice generates bits
alice_bits = randint(2, size=n)
## Step 2
# Create an array to tell us which qubits
# are encoded in which bases
alice_bases = randint(2, size=n)
message = encode_message(alice_bits, alice_bases)
## Step 3
# Decide which basis to measure in:
bob_bases = randint(2, size=n)
bob_results = measure_message(message, bob_bases)
## Step 4
alice_key = remove_garbage(alice_bases, bob_bases, alice_bits)
print(alice_key)
Alice's Knowledge | Over Eve's Channel | Bob's Knowledge |
---|---|---|
alice_bits | ||
alice_bases | ||
message | message | message |
bob_bases | ||
bob_results | ||
alice_bases | alice_bases | |
bob_bases | bob_bases | |
alice_key |
np.random.seed(seed=0)
n = 100
## Step 1
# Alice generates bits
alice_bits = randint(2, size=n)
## Step 2
# Create an array to tell us which qubits
# are encoded in which bases
alice_bases = randint(2, size=n)
message = encode_message(alice_bits, alice_bases)
## Step 3
# Decide which basis to measure in:
bob_bases = randint(2, size=n)
bob_results = measure_message(message, bob_bases)
## Step 4
alice_key = remove_garbage(alice_bases, bob_bases, alice_bits)
bob_key = remove_garbage(alice_bases, bob_bases, bob_results)
print(bob_key)
Alice's Knowledge | Over Eve's Channel | Bob's Knowledge |
---|---|---|
alice_bits | ||
alice_bases | ||
message | message | message |
bob_bases | ||
bob_results | ||
alice_bases | alice_bases | |
bob_bases | bob_bases | |
alice_key | bob_key |
def sample_bits(bits, selection):
sample = []
for i in selection:
# use np.mod to make sure the
# bit we sample is always in
# the list range
i = np.mod(i, len(bits))
# pop(i) removes the element of the
# list at index 'i'
sample.append(bits.pop(i))
return sample
アリスとボブは比較した部分を公開します。公開した部分はもはや秘密でなくなってしまうので、鍵から除去します:
np.random.seed(seed=0)
n = 100
## Step 1
# Alice generates bits
alice_bits = randint(2, size=n)
## Step 2
# Create an array to tell us which qubits
# are encoded in which bases
alice_bases = randint(2, size=n)
message = encode_message(alice_bits, alice_bases)
## Step 3
# Decide which basis to measure in:
bob_bases = randint(2, size=n)
bob_results = measure_message(message, bob_bases)
## Step 4
alice_key = remove_garbage(alice_bases, bob_bases, alice_bits)
bob_key = remove_garbage(alice_bases, bob_bases, bob_results)
## Step 5
sample_size = 15
bit_selection = randint(n, size=sample_size)
bob_sample = sample_bits(bob_key, bit_selection)
print(" bob_sample = " + str(bob_sample))
alice_sample = sample_bits(alice_key, bit_selection)
print("alice_sample = "+ str(alice_sample))
Alice's Knowledge | Over Eve's Channel | Bob's Knowledge |
---|---|---|
alice_bits | ||
alice_bases | ||
message | message | message |
bob_bases | ||
bob_results | ||
alice_bases | alice_bases | |
bob_bases | bob_bases | |
alice_key | bob_key | |
bob_sample | bob_sample | bob_sample |
alice_sample | alice_sample | alice_sample |
プロトコルが妨害されずに正しく機能していれば、両者のサンプルは一致するはずです:
bob_sample == alice_sample
サンプルが一致すれば、(高い確率で) alice_key == bob_key
であるということになります。こうして、2人はメッセージの暗号化に使うことができる秘密鍵を共有しました!
Alice's Knowledge | Over Eve's Channel | Bob's Knowledge |
---|---|---|
alice_bits | ||
alice_bases | ||
message | message | message |
bob_bases | ||
bob_results | ||
alice_bases | alice_bases | |
bob_bases | bob_bases | |
alice_key | bob_key | |
bob_sample | bob_sample | bob_sample |
alice_sample | alice_sample | alice_sample |
shared_key | shared_key |
print(bob_key)
print(alice_key)
print("key length = %i" % len(alice_key))
np.random.seed(seed=3)
np.random.seed(seed=3)
## Step 1
alice_bits = randint(2, size=n)
print(alice_bits)
np.random.seed(seed=3)
## Step 1
alice_bits = randint(2, size=n)
## Step 2
alice_bases = randint(2, size=n)
message = encode_message(alice_bits, alice_bases)
print(alice_bases)
この例の場合、アリスのメッセージの最初の量子ビットは $|+\rangle$ 状態です:
message[0].draw()
np.random.seed(seed=3)
## Step 1
alice_bits = randint(2, size=n)
## Step 2
alice_bases = randint(2, size=n)
message = encode_message(alice_bits, alice_bases)
## Interception!!
eve_bases = randint(2, size=n)
intercepted_message = measure_message(message, eve_bases)
print(intercepted_message)
ゼロ番目の量子ビットの場合を以下に示します。イブがランダムに選んだ基底はアリスが選んだものと同じではないため、量子ビットは $|+\rangle$ の状態から、$Z$基底では50%の確率で $|0\rangle$ または $|1\rangle$ の状態に変化します:
message[0].draw()
np.random.seed(seed=3)
## Step 1
alice_bits = randint(2, size=n)
## Step 2
alice_bases = randint(2, size=n)
message = encode_message(alice_bits, alice_bases)
## Interception!!
eve_bases = randint(2, size=n)
intercepted_message = measure_message(message, eve_bases)
## Step 3
bob_bases = randint(2, size=n)
bob_results = measure_message(message, bob_bases)
message[0].draw()
np.random.seed(seed=3)
## Step 1
alice_bits = randint(2, size=n)
## Step 2
alice_bases = randint(2, size=n)
message = encode_message(alice_bits, alice_bases)
## Interception!!
eve_bases = randint(2, size=n)
intercepted_message = measure_message(message, eve_bases)
## Step 3
bob_bases = randint(2, size=n)
bob_results = measure_message(message, bob_bases)
## Step 4
bob_key = remove_garbage(alice_bases, bob_bases, bob_results)
alice_key = remove_garbage(alice_bases, bob_bases, alice_bits)
np.random.seed(seed=3)
## Step 1
alice_bits = randint(2, size=n)
## Step 2
alice_bases = randint(2, size=n)
message = encode_message(alice_bits, alice_bases)
## Interception!!
eve_bases = randint(2, size=n)
intercepted_message = measure_message(message, eve_bases)
## Step 3
bob_bases = randint(2, size=n)
bob_results = measure_message(message, bob_bases)
## Step 4
bob_key = remove_garbage(alice_bases, bob_bases, bob_results)
alice_key = remove_garbage(alice_bases, bob_bases, alice_bits)
## Step 5
sample_size = 15
bit_selection = randint(n, size=sample_size)
bob_sample = sample_bits(bob_key, bit_selection)
print(" bob_sample = " + str(bob_sample))
alice_sample = sample_bits(alice_key, bit_selection)
print("alice_sample = "+ str(alice_sample))
bob_sample == alice_sample
ダメです。ボブの鍵とアリスの鍵は一致しません。イブがステップ2とステップ3の間でメッセージを読もうと試み、量子ビットの状態を変えたためである、ということが私たちにはわかっています。アリスとボブが知りうる範囲においては、これは通信路のノイズによるものであるかもしれません。しかしいずれにしろ、2人は結果をすべて捨て去り、やり直さなければなりません―イブの盗聴の試みは失敗したのです。
5. リスク分析
イブがすべての量子ビットを測定するこのタイプの盗聴においては、ボブとアリスのサンプルが一致し、そのためにアリスが脆弱なメッセージをイブの通信路で送ってしまう可能性がわずかにあります。この確率を計算し、量子鍵配送がどの程度危険であるかを見てみましょう。
- アリスとボブが量子ビットの結果を使うためには、2人が同じ基底を選んでいる必要があります。イブもこの基底を選んだ場合には、誤りを引き起こすことなく盗聴に成功します。これが起こる可能性は50%です。
- イブが 間違った 基底、つまりアリスやボブとは異なった基底を選んだ場合、ボブには依然として、アリスが送ろうとした値を測定する可能性が50%あります。この場合には、盗聴もまた検出されません。
- しかしイブが 間違った 基底、つまりアリスやボブとは異なった基底を選んだ場合、50%の確率で、アリスが送ろうとした値をボブは測定で得られないことになります。これにより、2人の鍵には違いが 生じます 。
アリスとボブが鍵のうちの1ビットを比較すると、ビットが一致する確率は $0.75$ であり、一致すればイブの盗聴には気が付かないでしょう。2ビットを比較すれば、盗聴が気づかれない可能性は $0.75^2 = 0.5625$ となります。アリスとボブが比較するのに選んだビット数 ($x$) によって、イブが気づかれない可能性を次のように計算できることがわかります:
$$ P(\text{undetected}) = 0.75^x $$上で行ったような方法で15ビットを比較することにすれば、イブが気づかれない可能性は1.3%です。これでは危険すぎるというのであれば、15ビットでなく50ビットにしてもよいでしょう。そうすれば知られずに覗き見られてしまう可能性は0.00006%になります。
下に示したセルを実行することで、プロトコルをもう一度試してみることができます。 sample_size
を小さな値に変えて、アリスとボブの鍵をイブが盗聴するのがどのくらい容易になるかを見てみてください。
n = 100
# Step 1
alice_bits = randint(2, size=n)
alice_bases = randint(2, size=n)
# Step 2
message = encode_message(alice_bits, alice_bases)
# Interception!
eve_bases = randint(2, size=n)
intercepted_message = measure_message(message, eve_bases)
# Step 3
bob_bases = randint(2, size=n)
bob_results = measure_message(message, bob_bases)
# Step 4
bob_key = remove_garbage(alice_bases, bob_bases, bob_results)
alice_key = remove_garbage(alice_bases, bob_bases, alice_bits)
# Step 5
sample_size = 15 # Change this to something lower and see if
# Eve can intercept the message without Alice
# and Bob finding out
bit_selection = randint(n, size=sample_size)
bob_sample = sample_bits(bob_key, bit_selection)
alice_sample = sample_bits(alice_key, bit_selection)
if bob_sample != alice_sample:
print("Eve's interference was detected.")
else:
print("Eve went undetected!")
import qiskit.tools.jupyter
%qiskit_version_table