ECDSA
O sistema de assinatura digital que controla a posse dos bitcoins
O Bitcoin usa um sistema de assinatura digital chamado ECDSA para controlar a posse dos bitcoins.
Em resumo, um sistema de assinatura digital permite gerar o seu próprio par de chave privada/chave pública. Você pode então usar a chave privada para gerar assinaturas que provam que você é o dono da chave pública, sem ter que revelar a chave privada.
Este sistema é usado no Bitcoin para permitir que as pessoas recebam e enviem bitcoins em transações.
Qualquer um pode gerar seu próprio par de chaves, e então qualquer um pode enviar (ou "travar") uma saída para a sua chave pública. Ninguém pode roubar esses bitcoins, porque só a pessoa com a chave privada correta para esta chave pública consegue gerar assinaturas válidas para "destravar" os bitcoins e usá-los como uma entrada em uma transação futura.
Não sei o suficiente sobre criptografia para explicar por que o ECDSA funciona, mas posso mostrar como o ECDSA funciona.
Código ECDSA Completo
require "digest" # para hashear dados de transação para poder assiná-los
require "securerandom" # para gerar nonces aleatórios ao assinar
# -------------------------
# Parâmetros da Curva Elíptica
# -------------------------
# y² = x³ + ax + b
$a = 0
$b = 7
# corpo primo
$p = 115792089237316195423570985008687907853269984665640564039457584007908834671663 #=> 2**256 - 2**32 - 2**9 - 2**8 - 2**7 - 2**6 - 2**4 - 1
# número de pontos na curva que podemos alcançar ("ordem")
$n = 115792089237316195423570985008687907852837564279074904382605163141518161494337
# ponto gerador (o ponto inicial na curva usado para todos os cálculos)
$G = {
x: 55066263022277343669578718895168534326250603453777594175500187360389116729240,
y: 32670510020758816978083085130507043184471273380659243275938904335757337482424,
}
# ---------------
# Inverso Modular: Ruby não tem uma função modinv nativa
# ---------------
def inverse(a, m = $p)
# armazenar módulo original
m_orig = m
# garantir que a seja positivo
if a < 0
a = a % m
end
# definir valores iniciais antes do loop
y_prev = 0
y = 1
while a > 1
q = m / a
y_before = y # armazenar valor atual de y
y = y_prev - q * y # calcular novo valor de y
y_prev = y_before # definir y anterior como o valor antigo de y
a_before = a # armazenar valor atual de a
a = m % a # calcular novo valor de a
m = a_before # definir m como o valor antigo de a
end
return y % m_orig
end
# ------
# Double: somar um ponto a ele mesmo
# ------
def double(point)
# slope = (3x₁² + a) / 2y₁
slope = ((3 * point[:x] ** 2 + $a) * inverse((2 * point[:y]), $p)) % $p # usando inverse para ajudar com a divisão
# x = slope² - 2x₁
x = (slope ** 2 - (2 * point[:x])) % $p
# y = slope * (x₁ - x) - y₁
y = (slope * (point[:x] - x) - point[:y]) % $p
# Retornar o novo ponto
return { x: x, y: y }
end
# ---
# Add: somar dois pontos
# ---
def add(point1, point2)
# duplicar se ambos os pontos forem iguais
if point1 == point2
return double(point1)
end
# slope = (y₁ - y₂) / (x₁ - x₂)
slope = ((point1[:y] - point2[:y]) * inverse(point1[:x] - point2[:x], $p)) % $p
# x = slope² - x₁ - x₂
x = (slope ** 2 - point1[:x] - point2[:x]) % $p
# y = slope * (x₁ - x) - y₁
y = ((slope * (point1[:x] - x)) - point1[:y]) % $p
# Retornar o novo ponto
return { x: x, y: y }
end
# --------
# Multiply: usar operações double e add para multiplicar rapidamente um ponto por um valor inteiro (ex.: chave privada)
# --------
def multiply(k, point = $G)
# criar uma cópia do ponto inicial (para uso na adição depois)
current = point
# converter inteiro para representação binária
binary = k.to_s(2)
# algoritmo double-and-add para multiplicação rápida
binary.split("").drop(1).each do |char| # da esquerda para a direita, ignorando o primeiro caractere binário
# 0 = double
current = double(current)
# 1 = double e add
current = add(current, point) if char == "1"
end
# retornar o ponto final
current
end
# ----
# Sign
# ----
def sign(private_key, hash, nonce = nil)
# gerar número aleatório se não for fornecido
if nonce == nil
loop do
nonce = SecureRandom.hex(32).to_i(16)
break if nonce < $n # garantir que o número aleatório esteja abaixo do número de pontos da curva
end
end
# r = o valor x de um ponto aleatório na curva
r = multiply(nonce)[:x] % $n
# s = nonce⁻¹ * (hash + private_key * r) mod n
s = (inverse(nonce, $n) * (hash + private_key * r)) % $n # isto quebra a linearidade (comparado ao schnorr)
# assinatura é [r, s]
return { r: r, s: s }
end
# ------
# Verify
# ------
def verify(public_key, signature, hash)
# ponto 1
point1 = multiply(inverse(signature[:s], $n) * hash)
# ponto 2
point2 = multiply((inverse(signature[:s], $n) * signature[:r]), public_key)
# somar os dois pontos
point3 = add(point1, point2)
# verificar se a coordenada x deste ponto coincide com a coordenada x do ponto aleatório fornecido
return point3[:x] % $n == signature[:r] # primeiro é preciso mod a coordenada x com $n
end
# --------------------------
# Criar uma Chave Pública
# --------------------------
# Exemplo de chave privada (em hexadecimal)
private_key = "f94a840f1e1a901843a75dd07ffcc5c84478dc4f987797474c9393ac53ab55e6"
# Chave pública é o ponto gerador multiplicado pela chave privada
point = multiply(private_key.to_i(16))
# converter valores x e y do ponto da chave pública para hexadecimal
x = point[:x].to_s(16).rjust(64, "0") # preencher com zeros para garantir 64 caracteres (32 bytes)
y = point[:y].to_s(16).rjust(64, "0")
# chave pública não-compactada (usa coordenadas x e y completas) FORMATO ANTIGO!
public_key_uncompressed = "04" + x + y
# chave pública compactada (usa um prefixo para indicar se y é par ou ímpar)
if (point[:y] % 2 == 0)
public_key_compressed = "02" + x # y é par
else
public_key_compressed = "03" + x # y é ímpar
end
#puts public_key_compressed #=> 024aeaf55040fa16de37303d13ca1dde85f4ca9baa36e2963a27a1c0c1165fe2b1
# -----------------------
# Assinar uma Transação
# -----------------------
# Uma estrutura básica para armazenar os dados da transação
def tx(scriptsig)
# É preciso calcular um byte indicando o tamanho do próximo scriptsig em bytes (código aproximado, mas funciona)
size = (scriptsig.length / 2).to_s(16).rjust(2, "0")
# Dados brutos da transação não assinada com o campo scriptsig (você precisa saber a posição correta)
return "0100000001b7994a0db2f373a29227e1d90da883c6ce1cb0dd2d6812e4558041ebbbcfa54b00000000#{size}#{scriptsig}ffffffff01983a0000000000001976a914b3e2819b6262e0b1f19fc7229d75677f347c91ac88ac00000000"
end
# Chave privada e chave pública para os bitcoins bloqueados que queremos gastar
private_key = "f94a840f1e1a901843a75dd07ffcc5c84478dc4f987797474c9393ac53ab55e6" # sha256("learnmeabitcoin1")
public_key = "024aeaf55040fa16de37303d13ca1dde85f4ca9baa36e2963a27a1c0c1165fe2b1"
# NOTA: É preciso remover todas as assinaturas existentes dos dados da transação primeiro, se houver
# Colocar o scriptpubkey original como placeholder no scriptsig da entrada que você quer assinar (obrigatório)
scriptpubkey = "76a9144299ff317fcd12ef19047df66d72454691797bfc88ac" # apenas uma entrada nesta transação
transaction = tx(scriptpubkey)
# Anexar o tipo sighash aos dados da transação (obrigatório)
transaction = transaction + "01000000"
# Obter um hash dos dados da transação (porque assinamos o hash dos dados e não os dados em si)
hash = Digest::SHA256.hexdigest(Digest::SHA256.digest([transaction].pack("H*")))
# Usar a matemática de curvas elípticas para assinar o hash usando a chave privada e o nonce
signature = sign(private_key.to_i(16), hash.to_i(16), 123456789) # usando um nonce fixo para teste (inseguro)
# Usar o valor s baixo (BIP 62: Lidando com maleabilidade)
if (signature[:s] > $n / 2)
signature[:s] = $n - signature[:s]
end
# Codificar a assinatura no formato DER (formato um pouco estranho usado para assinaturas em transações bitcoin)
r = signature[:r].to_s(16).rjust(64, "0") # converter r para hexadecimal
s = signature[:s].to_s(16).rjust(64, "0") # converter s para hexadecimal
r = "00" + r if (r[0, 2].to_i(16) >= 0x80) # adicionar 00 se o primeiro byte for 0x80 ou superior (peculiaridade do DER)
s = "00" + s if (s[0, 2].to_i(16) >= 0x80) # adicionar 00 se o primeiro byte for 0x80 ou superior (peculiaridade do DER)
der = "" # string para armazenar nossa codificação DER
r_len = (r.length / 2).to_s(16).rjust(2, "0") # obter o tamanho de r (em bytes)
s_len = (s.length / 2).to_s(16).rjust(2, "0") # obter o tamanho de s (em bytes)
der << "02" << r_len << r << "02" << s_len << s # Adicionar à codificação DER (0x02 indica um tipo inteiro em DER)
der_len = (der.length / 2).to_s(16).rjust(2, "0") # obter o tamanho dos dados DER (em bytes)
der = "30" + der_len + der # Codificação DER final (0x30 indica um tipo de objeto composto)
# Anexar o tipo sighash à assinatura (obrigatório) (01 = ALL)
der = der + "01" # sem isso você recebe "mandatory-script-verify-flag-failed (Non-canonical DER signature) (code 16)"
# Construir o script de desbloqueio completo (scripts P2PKH precisam da chave pública original para a qual os bitcoins foram bloqueados): <tamanho> {assinatura} <tamanho> {chave_pública}
scriptsig = (der.length / 2).to_s(16) + der + (public_key.length / 2).to_s(16) + public_key
# Colocar o scriptsig completo nos dados da transação original
transaction = tx(scriptsig)
# Mostrar a transação assinada
#puts transaction #=> 0100000001b7994a0db2f373a29227e1d90da883c6ce1cb0dd2d6812e4558041ebbbcfa54b000000006a473044022008f4f37e2d8f74e18c1b8fde2374d5f28402fb8ab7fd1cc5b786aa40851a70cb02201f40afd1627798ee8529095ca4b205498032315240ac322c9d8ff0f205a93a580121024aeaf55040fa16de37303d13ca1dde85f4ca9baa36e2963a27a1c0c1165fe2b1ffffffff01983a0000000000001976a914b3e2819b6262e0b1f19fc7229d75677f347c91ac88ac00000000
# Enviar a transação para a rede bitcoin
# $ bitcoin-cli sendrawtransaction [dados da transação em hex] Curvas Elípticas
A espinha dorsal matemática do ECDSA
O ECDSA usa a curva elíptica como base para um sistema de assinatura digital.
Em resumo, chaves públicas e assinaturas são apenas pontos em uma curva elíptica. Se ambos os pontos forem criados a partir da mesma chave privada (um número grande), haverá uma conexão geométrica entre eles que prova que a pessoa que criou a assinatura também criou (ou "possui") a chave pública.
Não vou cobrir a matemática das curvas elípticas aqui, mas tudo o que precisamos para usar o ECDSA no Bitcoin é saber multiplicar um ponto na curva elíptica.
Multiplicação na Curva
Em suma, "multiplicação" na curva elíptica basicamente significa pegar um ponto inicial na curva e quicar em torno dele um certo número de vezes para terminar em um novo ponto da curva. A propriedade especial desta operação de "multiplicação" é que não é possível "voltar atrás", e é por isso que a curva elíptica é usada para sistemas criptográficos como assinaturas digitais.
De qualquer forma, aqui está o código para realizar a multiplicação na curva elíptica (usando os parâmetros da curva Secp256k1 usada no Bitcoin):
Parâmetros Secp256k1
# y² = x³ + ax + b
$a = 0
$b = 7
# corpo primo
$p = 115792089237316195423570985008687907853269984665640564039457584007908834671663 #=> 2**256 - 2**32 - 2**9 - 2**8 - 2**7 - 2**6 - 2**4 - 1
# número de pontos na curva que podemos alcançar ("ordem")
$n = 115792089237316195423570985008687907852837564279074904382605163141518161494337
# ponto gerador (o ponto inicial na curva usado para todos os cálculos)
$G = {
x: 55066263022277343669578718895168534326250603453777594175500187360389116729240,
y: 32670510020758816978083085130507043184471273380659243275938904335757337482424,
} Matemática da Curva Elíptica
def inverse(a, m = $p)
# armazenar módulo original
m_orig = m
# garantir que a seja positivo
if a < 0
a = a % m
end
# definir valores iniciais antes do loop
y_prev = 0
y = 1
while a > 1
q = m / a
y_before = y # armazenar valor atual de y
y = y_prev - q * y # calcular novo valor de y
y_prev = y_before # definir y anterior como o valor antigo de y
a_before = a # armazenar valor atual de a
a = m % a # calcular novo valor de a
m = a_before # definir m como o valor antigo de a
end
return y % m_orig
end
def add(point1, point2)
# duplicar se ambos os pontos forem iguais
if point1 == point2
return double(point1)
end
# slope = (y₁ - y₂) / (x₁ - x₂)
slope = ((point1[:y] - point2[:y]) * inverse(point1[:x] - point2[:x], $p)) % $p
# x = slope² - x₁ - x₂
x = (slope ** 2 - point1[:x] - point2[:x]) % $p
# y = slope * (x₁ - x) - y₁
y = ((slope * (point1[:x] - x)) - point1[:y]) % $p
# Retornar o novo ponto
return { x: x, y: y }
end
def double(point)
# slope = (3x₁² + a) / 2y₁
slope = ((3 * point[:x] ** 2 + $a) * inverse((2 * point[:y]), $p)) % $p # usando inverse para ajudar com a divisão
# x = slope² - 2x₁
x = (slope ** 2 - (2 * point[:x])) % $p
# y = slope * (x₁ - x) - y₁
y = (slope * (point[:x] - x) - point[:y]) % $p
# Retornar o novo ponto
return { x: x, y: y }
end
def multiply(k, point = $G)
# criar uma cópia do ponto inicial (para uso na adição depois)
current = point
# converter inteiro para representação binária
binary = k.to_s(2)
# algoritmo double-and-add para multiplicação rápida
binary.split("").drop(1).each do |char| # da esquerda para a direita, ignorando o primeiro caractere binário
# 0 = double
current = double(current)
# 1 = double e add
current = add(current, point) if char == "1"
end
# retornar o ponto final
current
end Uso
Como criar e verificar assinaturas digitais usando o ECDSA?
Agora que sabemos multiplicar um ponto na curva elíptica, podemos usar isto como base para um sistema de criação de assinaturas digitais.
O sistema a seguir é chamado de Elliptic Curve Digital Signature Algorithm, ou ECDSA para abreviar.
Geração de Chaves
Chave Privada
Multiplicação na Curva
Criamos pares de chaves usando a multiplicação na curva elíptica:
- Chave privada (
d) — Um número grande gerado aleatoriamente entre 0 e o número de pontos na curva ([0...n-1]) - Chave pública (
Q) — O ponto gerador (G) multiplicado pela chave privada (d).
d é a chave privada (um inteiro)
G é o ponto gerador (um ponto)
Q é a chave pública (um ponto)
Portanto, na criptografia de curva elíptica, uma chave privada é apenas um grande inteiro aleatório (menor que o número de pontos na curva), e sua chave pública correspondente é apenas um ponto na curva.
Por exemplo:
private key = 112757557418114203588093402336452206775565751179231977388358956335153294300646
public key = {
x: 33886286099813419182054595252042348742146950914608322024530631065951421850289,
y: 9529752953487881233694078263953407116222499632359298014255097182349749987176
} Função de Alçapão
Dado um ponto de chave pública Q, não há uma maneira fácil de descobrir a chave privada d usada para criá-lo.
A única maneira de descobrir a chave privada seria multiplicar manualmente o ponto gerador G por diferentes números para ver se você consegue obter a mesma chave pública, e esta abordagem de força bruta será impossivelmente lenta se alguém usou um número muito grande para sua chave privada.
Portanto, a multiplicação na curva elíptica é conhecida como uma função de alçapão (porque é fácil ir em uma direção mas difícil ir na outra), que é um componente chave de toda criptografia de chave pública.
Além disso, a conexão matemática unidirecional entre a chave privada e a chave pública significa que você pode usar ambas independentemente para calcular os mesmos pontos na curva elíptica posteriormente, o que é muito útil ao construir um sistema para criar assinaturas digitais.
Assinar
ECDSA (assinar)
Para assinar uma mensagem você precisa de três coisas:
- Número Aleatório (
k) — Isto introduz um elemento de aleatoriedade nas assinaturas, importante para a segurança. Significa que cada assinatura que gerarmos será diferente, mesmo se assinarmos a mesma mensagem duas vezes. - Hash da Mensagem (
z) — Este é o hash da mensagem que queremos assinar. Hashear a mensagem nos dá uma impressão digital pequena e única, e é mais eficiente assinar esta impressão digital do que assinar um grande bloco de dados. Você tem uma escolha de qual algoritmo de hash usar, mas o mais comumente usado com secp256k1 é o SHA-256. - Chave Privada (
d) — A fonte de uma chave pública (que tornamos publicamente disponível).
Uma assinatura é composta de duas partes:
r— Um ponto aleatório na curva. Pegamos o número aleatórioke multiplicamos pelo ponto gerador para obter um ponto aleatórioR. Usamos apenas a coordenada x deste ponto, e chamamos der.s— Um número para acompanhar o ponto aleatório. É um número único criado a partir da combinação do hash da mensagemze da chave privadad, vinculado ao ponto aleatório usandor.
Uma assinatura ECDSA contém a coordenada x de um ponto aleatório na curva.
Equação de Assinatura ECDSA
A notação ⁻¹ indica o inverso modular daquele número. Aqui o inverso multiplicativo modular é encontrado mod n (o número de pontos na curva).
Estes dois valores [r, s] são a "assinatura digital".
Por exemplo:
número aleatório (k): 12345
mensagem: ECDSA é a coisa mais divertida que já experimentei
sha256(mensagem) (z): 103318048148376957923607078689899464500752411597387986125144636642406244063093
chave privada (d): 112757557418114203588093402336452206775565751179231977388358956335153294300646
ponto aleatório (k*G = R): {
x = 108607064596551879580190606910245687803607295064141551927605737287325610911759,
y = 6661302038839728943522144359728938428925407345457796456954441906546235843221
}
assinatura: r = R[x], s = k⁻¹ * (z + r * d): {
r = 108607064596551879580190606910245687803607295064141551927605737287325610911759,
s = 73791001770378044883749956175832052998232581925633570497458784569540878807131
} Nonce: Um número aleatório em criptografia é às vezes chamado de nonce, abreviação de number used once.
Conversor de Números
Em resumo, o valor único s fornece um caminho para chegar ao ponto gerado aleatoriamente r.
Você pode dar estas duas informações para outra pessoa e, partindo do ponto da chave pública Q, ela pode usar o valor s para ajudá-la a chegar ao ponto aleatório r. O truque aqui é que somente a pessoa com a chave privada correspondente d poderia criar um caminho válido para este ponto aleatório fornecido por s.
Este caminho também tem o hash da mensagem z codificado, que é o que efetivamente nos permite criar assinaturas para mensagens; ninguém pode criar os caminhos da chave pública para um ponto aleatório na curva através do hash da mensagem sem conhecer a chave privada da qual foi criado.
ECDSA Sign
def sign(private_key, hash, nonce = nil)
# gerar número aleatório se não for fornecido
if nonce == nil
loop do
nonce = SecureRandom.hex(32).to_i(16)
break if nonce < $n # garantir que o número aleatório esteja abaixo do número de pontos da curva
end
end
# r = o valor x de um ponto aleatório na curva
r = multiply(nonce)[:x] % $n
# s = nonce⁻¹ * (hash + private_key * r) mod n
s = (inverse(nonce, $n) * (hash + private_key * r)) % $n # isto quebra a linearidade (comparado ao schnorr)
# assinatura é [r, s]
return { r: r, s: s }
end Recuperação de Chave Privada
Se você usar o mesmo ponto aleatório (isto é, o mesmo valor para k) em mais de uma assinatura, qualquer um pode descobrir sua chave privada.
Por exemplo, digamos que temos duas mensagens assinadas que foram geradas usando o mesmo valor para k.
Para cada mensagem assinada temos o hash da mensagem z, e também os valores r e s de cada uma das respectivas assinaturas:
Mensagem Assinada 1: (z₁, r₁, s₁)
Mensagem Assinada 2: (z₂, r₂, s₂) No entanto, como o mesmo valor para k foi usado cada vez para gerar o ponto aleatório (R = k*G), o valor r (coordenada x de R) em cada uma dessas assinaturas também será o mesmo:
Mensagem Assinada 1: (z₁, r, s₁)
Mensagem Assinada 2: (z₂, r, s₂) Então, como podemos usar esta informação para descobrir a chave privada d?
Em primeiro lugar, sabemos que o valor s em cada uma destas assinaturas foi calculado usando s = k⁻¹(z + r * d) mod n, então:
s₁ = k⁻¹(z₁ + r * d) mod n
s₂ = k⁻¹(z₂ + r * d) mod n E graças ao fato de ambas as equações agora terem o mesmo valor para k, podemos resolvê-las como um par de equações simultâneas para descobrir o valor de k.
Para fazer isso, começamos reorganizando a segunda equação para isolar r * d:
s₂ = k⁻¹(z₂ + r * d) mod n
r * d = k * s₂ - z₂ mod n Então podemos substituir isto na primeira equação e reorganizá-la para obter k:
s₁ = k⁻¹(z₁ + r * d) mod n
s₁ = k⁻¹(z₁ + (k * s₂ - z₂)) mod n
k = (z₁ - z₂) * (s₁ - s₂)⁻¹ mod n Lembre-se de que multiplicar por (s₁ - s₂)⁻¹ significa multiplicar pelo inverso multiplicativo modular de (s₁ - s₂), que é a mesma coisa que "divisão" na matemática de curvas elípticas.
E depois de descobrirmos k, podemos usá-lo em s = k⁻¹(z + r * d) mod n novamente para descobrir d.
Então, reorganizando a primeira equação (você pode usar qualquer uma) para isolar d:
s₁ = k⁻¹(z₁ + r * d) mod n
d = (k * s₁ - z₁) * r⁻¹ mod n E como já conhecíamos (z₁, r, s₁) e acabamos de descobrir k, podemos inserir todos eles nesta equação para descobrir a chave privada d.
Em notação matemática, a recuperação de chave privada fica assim:
Portanto, certifique-se de sempre usar valores seguramente aleatórios para k cada vez que criar uma assinatura. Se alguém perceber que você usou o mesmo valor r ao assinar mensagens diferentes para a mesma chave pública, leva apenas milissegundos para recuperar sua chave privada.
Em 2011, hackers conseguiram obter a chave privada do PS3 porque a Sony estava usando o mesmo valor para k ao gerar suas assinaturas.
Aqui está um exemplo de recuperação de uma chave privada a partir de duas assinaturas usando o mesmo k em Ruby:
require 'digest' # usado para hashear mensagens antes de assinar
# Nota: Este código usa as funções inverse(), double(), add(), multiply() e sign() definidas anteriormente
# --------------
# Assinar Mensagens
# --------------
# 0. Chaves
prv = 1111222233334444555566667777888899990000 # qualquer chave privada antiga
pub = multiply(prv)
# 1. Criar primeira mensagem assinada
k = 800000
z1 = Digest::SHA256.hexdigest("Just a simple message.").to_i(16)
sig1 = sign(prv, z1, k)
# 2. Criar segunda mensagem assinada
k = 800000 # Usando o mesmo valor para k!
z2 = Digest::SHA256.hexdigest("I have used the same k value.").to_i(16)
sig2 = sign(prv, z2, k)
# --------------------
# Recuperação de Chave Privada
# --------------------
# k = (z₁ - z₂) * (s1 - s₂)⁻¹ mod n
# d = (k * s₁ - z₁) * r⁻¹ mod n
# 1. Calcular k (nota: o resultado pode ser o inverso aditivo do k original, mas ainda funciona)
k_calculated = ((z1 - z2) * inverse(sig1[:s] - sig2[:s], $n)) % $n
# 2. Calcular d (a chave privada original)
d_calculated = ((k_calculated * sig1[:s] - z1) * inverse(sig1[:r], $n)) % $n
puts d_calculated #=> 1111222233334444555566667777888899990000 Verificar
ECDSA (verificar)
Você pode verificar uma mensagem e sua assinatura com três coisas:
- Chave Pública
Q— Esta é a chave pública da pessoa que alega ter criado a assinatura. - Mensagem — Os dados que foram assinados. Nós podemos hasheá-los nós mesmos para obter o hash da mensagem
z. - Assinatura
[r, s]— A assinatura criada para a mensagem acima, supostamente criada pela pessoa que tem a chave privada da chave pública.
Em seguida, usamos estes três dados para calcular dois pontos na curva:
- Ponto 1. Comece com o ponto gerador
Ge multiplique porinverse(s) * z. - Ponto 2. Comece com o ponto da chave pública
Qe multiplique porinverse(s) * r.
Podemos agora somar estes dois pontos para nos dar Ponto 3:
Verificação ECDSA na curva elíptica.
Equação de Verificação ECDSA
Se o Ponto 3 corresponder ao ponto aleatório, a assinatura é válida.
Por exemplo:
mensagem: ECDSA é a coisa mais divertida que já experimentei
sha256(mensagem) (z): 103318048148376957923607078689899464500752411597387986125144636642406244063093
assinatura (r,s): {
r = 108607064596551879580190606910245687803607295064141551927605737287325610911759,
s = 73791001770378044883749956175832052998232581925633570497458784569540878807131
}
chave pública (Q): {
x = 33886286099813419182054595252042348742146950914608322024530631065951421850289,
y = 9529752953487881233694078263953407116222499632359298014255097182349749987176
}
verificação (s⁻¹ * z)G + (s⁻¹ * r)Q: {
x = 108607064596551879580190606910245687803607295064141551927605737287325610911759, <- corresponde a r (coordenada x do ponto aleatório)
y = 6661302038839728943522144359728938428925407345457796456954441906546235843221
}
Conversor de Números
Chave Pública
Em outras palavras, a assinatura para esta mensagem só poderia ter sido criada pela pessoa que tem a chave privada real da qual a chave pública foi criada. Ninguém mais pode lhe dar um valor s que você possa usar em combinação com a chave pública Q para alcançar o ponto aleatório R a menos que conhecesse a chave privada d para aquela chave pública.
Se você alterar o conteúdo da mensagem assinada ou tentar usar a assinatura com uma chave pública diferente, o terceiro ponto resultante não corresponderá ao ponto aleatório fornecido na assinatura, e a verificação da assinatura falhará.
ECDSA Verify
def verify(public_key, signature, hash)
# ponto 1
point1 = multiply(inverse(signature[:s], $n) * hash)
# ponto 2
point2 = multiply((inverse(signature[:s], $n) * signature[:r]), public_key)
# somar os dois pontos
point3 = add(point1, point2)
# verificar se a coordenada x deste ponto coincide com a coordenada x do ponto aleatório fornecido
return point3[:x] % $n == signature[:r] # primeiro é preciso mod a coordenada x com $n
end Por que isto funciona? (Matemática)
Assinando:
A pessoa que cria uma assinatura começa usando um número aleatório k para gerar um ponto aleatório na curva:
R = k * G Em seguida, calcula um número auxiliar usando sua chave privada d e o hash da mensagem z (junto com r (a coordenada x de R) e o número aleatório k):
s = k⁻¹ * (z + r * d) Verificando:
A seguinte equação permite calcular o mesmo ponto usando a chave pública Q junto com o hash da mensagem z e o valor s fornecido:
R = (s⁻¹ * z)G + (s⁻¹ * r)Q Podemos agora reorganizar esta equação e substituir alguns valores para provar que esta equação de fato nos leva ao mesmo ponto.
Para começar, a chave pública Q é d * G, então:
R = (s⁻¹ * z)G + (s⁻¹ * r)d*G Se reorganizarmos esta equação, obtemos:
R = (s⁻¹ * z)G + (s⁻¹ * r * d)G
R = s⁻¹ * (z + r * d) * G Agora, lembre-se que s = k⁻¹ * (z + r * d). Se reorganizarmos isto para isolar k, obtemos k = s⁻¹ * (z + r * d), e substituindo isto na equação acima:
R = k * G E esse é o mesmo cálculo que foi usado para gerar o ponto aleatório em primeiro lugar.
Resumo
Considerações finais
A melhor maneira de entender o ECDSA é tentar codificá-lo você mesmo.
A parte mais difícil geralmente não é a matemática da curva elíptica, mas sim preparar e formatar as assinaturas resultantes para uso dentro de transações bitcoin posteriormente. Além disso, nem sempre é fácil trabalhar com números grandes em algumas linguagens de programação, então você pode precisar usar funções especiais para realizar as operações de curva elíptica.
Fora isso, o código não é tão difícil quanto você pode ter pensado inicialmente.
É claro que eu não recomendaria usar este código em seu sistema mission-critical mais recente, mas deve ajudá-lo a começar a criar suas próprias chaves públicas e assinar suas próprias transações no Bitcoin sem usar uma biblioteca ECDSA, caso você queira.
Divirta-se.
Não era necessário que Satoshi Nakamoto soubesse os detalhes de como os sistemas de assinatura digital funcionam para poder criar o Bitcoin. Tudo o que ele precisava saber era que funciona, e que poderia usá-lo como mecanismo para enviar e receber dinheiro no sistema que estava construindo. A primeira versão do Bitcoin usava a biblioteca OpenSSL para fornecer a funcionalidade de criação e verificação de assinaturas digitais, então não foi algo que ele codificou manualmente.
Recursos
Leituras adicionais e referências
Referências:
- NIST.FIPS.186-4.pdf — Digital Signature Standard oficial do NIST. Contém diretrizes para DSA, RSA e ECDSA.
- sec2-v2.pdf — Lista de curvas recomendadas para uso em criptografia de curva elíptica da SECG. Contém parâmetros para a curva
secp256k1usada no Bitcoin.
Explicações:
- How The ECDSA Algorithm Works — Explicação concisa do ECDSA.
- Recovering private key from Secp256k1 signatures — Explicação matemática sucinta por Thomas Pornin sobre como recuperar uma chave privada se alguém usou o mesmo ponto aleatório para suas assinaturas mais de uma vez.
Código
Aqui estão algumas implementações de ECDSA em diferentes linguagens de programação que achei úteis: