ECDSA

O sistema de assinatura digital que controla a posse dos bitcoins

🛠️ Tradução em andamento — esta página mostra o conteúdo básico; a versão integral será publicada em breve.
Diagrama mostrando como o ECDSA é usado para travar e destravar saídas em transações bitcoin.

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

Animação mostrando como multiplicar um ponto em uma curva elíptica.

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.

Ícone Ferramenta Multiplicação na Curva

Multiplicação de Ponto (EC Multiply)

Multiplique um ponto na curva elíptica secp256k1.

Ponto 1
x:
0d
y:
0d
0d
Ponto 1 × Multiplicador
x:
0d
y:
0d

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

Ícone Ferramenta Chave Privada

Chave Privada

Gere um número aleatório de 256 bits.

Bits

0b
0 bits
0d
0x
0 bytes

Nunca use uma chave privada gerada por um site, nem digite sua chave privada em um site. Sites podem facilmente salvar a chave privada e usá-la para roubar seus bitcoins.

Ícone Ferramenta Multiplicação na Curva

Multiplicação de Ponto (EC Multiply)

Multiplique um ponto na curva elíptica secp256k1.

Ponto 1
x:
0d
y:
0d
0d
Ponto 1 × Multiplicador
x:
0d
y:
0d

Criamos pares de chaves usando a multiplicação na curva elíptica:

Animação mostrando como criar uma chave pública multiplicando o ponto gerador por uma chave privada.

d é a chave privada (um inteiro)
G é o ponto gerador (um ponto)
Q é a chave pública (um ponto)

Equação para multiplicar um ponto em uma curva elíptica.

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

Diagrama mostrando a função de alçapão da multiplicação na curva elíptica.

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

Ícone Ferramenta ECDSA (assinar)

Assinatura ECDSA

Assine o hash de uma mensagem usando uma chave privada.

Normalmente é o hash de dados de uma transação (já preparados para assinatura)

0x
0 bytes
0x
0x
0 bytes
Assinatura
0d
0d
High: Low:

Nunca insira sua chave privada em um site, nem use uma chave privada gerada por um site. Sites podem facilmente salvar a chave privada e usá-la para roubar seus bitcoins.

Para assinar uma mensagem você precisa de três coisas:

  1. 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.
  2. 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.
  3. Chave Privada (d) — A fonte de uma chave pública (que tornamos publicamente disponível).

Uma assinatura é composta de duas partes:

Animação mostrando um ponto aleatório sendo usado como parte da assinatura no ECDSA.

Uma assinatura ECDSA contém a coordenada x de um ponto aleatório na curva.

Equação para criar uma assinatura no ECDSA.

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.

Ícone Ferramenta Conversor de Números

Conversor de Números

Converta um número entre bases diferentes (binário, decimal, hexadecimal).

0b
0 dígitos
0d
0 dígitos
0x
0 dígitos

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:

Equação para recuperação de chave privada no ECDSA.

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

Ícone Ferramenta ECDSA (verificar)

Verificação ECDSA

Verifique uma assinatura usando o hash de uma mensagem e uma chave pública.

0x
0 bytes
Assinatura
0d
0d
0x
0 bytes
Verificação da Assinatura
0d
0d

Você pode verificar uma mensagem e sua assinatura com três coisas:

  1. Chave Pública Q — Esta é a chave pública da pessoa que alega ter criado a assinatura.
  2. Mensagem — Os dados que foram assinados. Nós podemos hasheá-los nós mesmos para obter o hash da mensagem z.
  3. 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:

Podemos agora somar estes dois pontos para nos dar Ponto 3:

Animação mostrando a verificação de uma assinatura ECDSA na curva elíptica.

Verificação ECDSA na curva elíptica.

Equação para verificar uma assinatura no ECDSA.

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
}
Ícone Ferramenta Conversor de Números

Conversor de Números

Converta um número entre bases diferentes (binário, decimal, hexadecimal).

0b
0 dígitos
0d
0 dígitos
0x
0 dígitos
Ícone Ferramenta Chave Pública

Chave Pública

Calcule a chave pública a partir de uma chave privada.

0 bytes

Chave Pública

Coordenadas
0d
0d

Uma chave pública é apenas um ponto em uma curva elíptica. A chave pública final são essas coordenadas em hexadecimal.

Compressão

A curva é simétrica no eixo x, então a chave comprimida guarda só a coordenada x e se o y é par ou ímpar. A x-only é usada em saídas Taproot.

0 bytes

Nunca digite sua chave privada em um site, nem use uma chave privada gerada por um site. Sites podem facilmente salvar a chave privada e usá-la para roubar seus bitcoins.

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:

Explicações:

Código

Aqui estão algumas implementações de ECDSA em diferentes linguagens de programação que achei úteis: