Skip to content
Lucas Penido edited this page Oct 24, 2021 · 12 revisions

1. Sobre o Ed521

O algoritmo Ed521 utiliza a forma de Edward da curva E-521 definida no documento EdDSA for more curves. Os parâmetros dessa curva são:

  • p : 2^521 - 1 = 0x1FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
  • l (ordem) : 2^519 - 337554763258501705789107630418782636071904961214051226618635150085779108655765 = 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD15B6C64746FC85F736B8AF5E7EC53F04FBD8C4569A8F1F4540EA2435F5180D6B
  • a : 0x01
  • d : -376014
  • X(G) (coordenada x do ponto gerador) : 0x752CB45C48648B189DF90CB2296B2878A3BFD9F42FC6C818EC8BF3C9C0C6203913F6ECC5CCC72434B1AE949D568FC99C6059D0FB13364838AA302A940A2F19BA6C
  • Y(G) (coordenada y do ponto gerador) : 0x0C
  • cofator : 4
  • Função hash utilizada: Shake256

Essa curva é definida na classe TwistedEdwardCurve. Para utilizá-la é preciso instanciar dessa forma:

var ed521 = TwistedEdwardCurve.ed521();

2. Geração de chaves

Ed521 utiliza uma chave pública e privada de 66 bytes.

2.1. Chave privada

A chave privada é composta por 66 bytes gerados de forma criptograficamente aleatória e segura. Ela é definida pela classe EdPrivateKey e para gerá-la de forma aleatória é preciso chamar o método generate e passar como parâmetro a curva que está sendo utilizada:

EdPrivateKey privateKey = EdPrivateKey.generate(ed521);

Outra forma de instanciar uma chave privada é utilizando o método fromBytes e passar como parâmetro uma cadeia de bytes (Uint8List) e a curva que está sendo utilizada:

EdPrivateKey privateKey = EdPrivateKey.fromBytes(privateKeyBytes, ed521);

Para esse exemplo é utilizado a seguinte chave privada em hexadecimal: 0x2367B29BCEACA04D6FE50D959DEE3D706F3A5F605CE5AAC0205C54CDDA59145C573B932814C7405770CD46171A0EB44C911F8E851ADEEFCC77E5841BF86E0B6486DD

2.2. Chave pública

A chave pública é definida pela classe EdPublicKey e pode ser obtida a partir da classe EdPrivateKey utilizando o método getPublicKey:

EdPublicKey publicKey = privateKey.getPublicKey();

Sua geração acontece em 4 etapas que serão demonstradas a seguir.

2.2.1. Hash

Realize o hash da chave privada utilizando SHAKE256 e recupere os 132 primeiros bytes, o resultado é armazenado em um buffer (h) de 132 bytes. Apenas os 66 bytes inferiores são utilizados para gerar a chave pública. Esses passos são realizados no método getPublicKey.

EdPublicKey getPublicKey() {
  // privateKey = 2367B29BCEACA04D6FE50D959DEE3D706F3A5F605CE5AAC0205C54CDDA59145C573B932814C7405770CD46171A0EB44C911F8E851ADEEFCC77E5841BF86E0B6486DD

  Uint8List h = this._hashPrivateKey();
  // h = F86EA93928B5EDC2163B90AC3184874865DFDEBD10450817E37EF646EB83E6A03746DDB8F72C863AE672ADE24BF5D3BF2FFB074FD3F5D71BB4D887D80FA560B2C440380F590C13657792D96FA5BF0379507CDD781EB47126F240B015D0C7ADBB297722E1B4D37C106CE7C7E1596708B333AAA9557E1937B3A6E6C7565B8CA9EAD764B506

  Uint8List leftHalf = h.sublist(0, this.curve.keySize);
  // leftHalf = F86EA93928B5EDC2163B90AC3184874865DFDEBD10450817E37EF646EB83E6A03746DDB8F72C863AE672ADE24BF5D3BF2FFB074FD3F5D71BB4D887D80FA560B2C440
  
  ...
}

2.2.2. Recuperando o escalar secreto (s)

A recuperação do escalar privado é feita pelo método _getPrivateScalar. Para recuperá-lo é necessário ajustar alguns elementos do buffer, que é realizado no método _pruningBuffer. Os dois bits menos significativos do primeiro byte são zerados, todos os oito bits do último byte são zerados e o bit mais alto do penúltimo byte é ajustado/setado. O resultado é então interpretado como Little Endian.

Uint8List _pruningBuffer(Uint8List buffer) {
  // buffer = F86EA93928B5EDC2163B90AC3184874865DFDEBD10450817E37EF646EB83E6A03746DDB8F72C863AE672ADE24BF5D3BF2FFB074FD3F5D71BB4D887D80FA560B2C440
  if (this.curve.curveName == 'Ed448' || this.curve.curveName == 'Ed521') {
    buffer[0] &= 0xFC;
    // buffer = F86EA93928B5EDC2163B90AC3184874865DFDEBD10450817E37EF646EB83E6A03746DDB8F72C863AE672ADE24BF5D3BF2FFB074FD3F5D71BB4D887D80FA560B2C440

    buffer[buffer.length - 1] = 0;
    // buffer = F86EA93928B5EDC2163B90AC3184874865DFDEBD10450817E37EF646EB83E6A03746DDB8F72C863AE672ADE24BF5D3BF2FFB074FD3F5D71BB4D887D80FA560B2C400

    buffer[buffer.length - 2] |= 0x80;
    // buffer = F86EA93928B5EDC2163B90AC3184874865DFDEBD10450817E37EF646EB83E6A03746DDB8F72C863AE672ADE24BF5D3BF2FFB074FD3F5D71BB4D887D80FA560B2C400
  } else {
    throw Exception("Curve not supported");
  }

  // buffer = F86EA93928B5EDC2163B90AC3184874865DFDEBD10450817E37EF646EB83E6A03746DDB8F72C863AE672ADE24BF5D3BF2FFB074FD3F5D71BB4D887D80FA560B2C400
  return buffer;
}

Uint8List _getPrivateScalar(leftHalf) {
  // leftHalf = F86EA93928B5EDC2163B90AC3184874865DFDEBD10450817E37EF646EB83E6A03746DDB8F72C863AE672ADE24BF5D3BF2FFB074FD3F5D71BB4D887D80FA560B2C440
  Uint8List a = _pruningBuffer(leftHalf);
  // a = F86EA93928B5EDC2163B90AC3184874865DFDEBD10450817E37EF646EB83E6A03746DDB8F72C863AE672ADE24BF5D3BF2FFB074FD3F5D71BB4D887D80FA560B2C400

  Uint8List s = toLittleEndian(a);
  // s = 00C4B260A50FD887D8B41BD7F5D34F07FB2FBFD3F54BE2AD72E63A862CF7B8DD4637A0E683EB46F67EE317084510BDDEDF6548878431AC903B16C2EDB52839A96EF8

  return s;
}

EdPublicKey getPublicKey() {
  ...

  // lefthalf = F86EA93928B5EDC2163B90AC3184874865DFDEBD10450817E37EF646EB83E6A03746DDB8F72C863AE672ADE24BF5D3BF2FFB074FD3F5D71BB4D887D80FA560B2C440
  Uint8List s = _getPrivateScalar(leftHalf);
  // s = 00C4B260A50FD887D8B41BD7F5D34F07FB2FBFD3F54BE2AD72E63A862CF7B8DD4637A0E683EB46F67EE317084510BDDEDF6548878431AC903B16C2EDB52839A96EF8

  ...
}

2.2.3. Ponto da chave pública

Para recuperar o ponto da chave pública, é preciso realizar a multiplicação por escalar do escalar secreto (s) com o ponto gerador da curva. Essa multiplicação é feita no método _getPublicKeyPoint.

Point _getPublicKeyPoint(s) {
  // s = 00C4B260A50FD887D8B41BD7F5D34F07FB2FBFD3F54BE2AD72E63A862CF7B8DD4637A0E683EB46F67EE317084510BDDEDF6548878431AC903B16C2EDB52839A96EF8

  Point publicKeyPoint = this.curve.generator.mul(s); // generator * s
  /*
  publicKeyPoint = {
    x: A7104267A1A89C35214F37DB17948CA9BB9F18671277BEAC3B49112BACD1530B93DE27BF67B952C8C8543619E9C3E6D625BBFEBB797730298C68AC2E33C61369E5,
    y: 1F03F7D772F60A2486A3EC7368B8864A2231DA143A491C950E776DA757992BC48757B642BC83410BFCC13D09C86B3074243A287022BA950E3B63D5D1E562D6FC872
  }
  */
  return publicKeyPoint;
}

EdPublicKey getPublicKey() {
  ...

  // s = 00C4B260A50FD887D8B41BD7F5D34F07FB2FBFD3F54BE2AD72E63A862CF7B8DD4637A0E683EB46F67EE317084510BDDEDF6548878431AC903B16C2EDB52839A96EF8
  Point publicKeyPoint = _getPublicKeyPoint(s);
  /*
  publicKeyPoint = {
    x: 00A7104267A1A89C35214F37DB17948CA9BB9F18671277BEAC3B49112BACD1530B93DE27BF67B952C8C8543619E9C3E6D625BBFEBB797730298C68AC2E33C61369E5,
    y: 01F03F7D772F60A2486A3EC7368B8864A2231DA143A491C950E776DA757992BC48757B642BC83410BFCC13D09C86B3074243A287022BA950E3B63D5D1E562D6FC872
  }
  */
  ...
}

2.2.4. Encoding do ponto da chave pública

A chave pública A é o encoding do ponto da chave pública. Primeiro codifique a coordenada y (no intervalo 0 <= y <p) como um little endian de 66 bytes. O bit mais significativo do último byte é sempre zero. Para realizar a codificação, copie o bit menos significativo da coordenada x para o bit mais significativo do último byte. O resultado é a chave pública. O encoding é realizado pelo método encodePoint definido na classe TwistedEdwardCurve.

// Método da classe TwistedEdwardCurve
Uint8List encodePoint(Point point) {
  /*
  point = {
    x: 00A7104267A1A89C35214F37DB17948CA9BB9F18671277BEAC3B49112BACD1530B93DE27BF67B952C8C8543619E9C3E6D625BBFEBB797730298C68AC2E33C61369E5,
    y: 01F03F7D772F60A2486A3EC7368B8864A2231DA143A491C950E776DA757992BC48757B642BC83410BFCC13D09C86B3074243A287022BA950E3B63D5D1E562D6FC872
  }
  */

  Uint8List encodedString = toLittleEndian(encodeBigInt(point.y, keySize));
  // encodedString = 72C86F2D561E5D3DB6E350A92B0287A2434207B3869CD013CCBF1034C82B647B7548BC927975DA76E750C991A443A11D23A264888B36C73E6A48A2602F777D3FF001

  if (point.x & BigInt.one == BigInt.one) {
    encodedString[encodedString.length - 1] |= 0x80;
  }
  // encodedString = 72C86F2D561E5D3DB6E350A92B0287A2434207B3869CD013CCBF1034C82B647B7548BC927975DA76E750C991A443A11D23A264888B36C73E6A48A2602F777D3FF081

  return encodedString;
}

EdPublicKey getPublicKey() {
  ...

  /*
  publicKeyPoint = {
    x: 00A7104267A1A89C35214F37DB17948CA9BB9F18671277BEAC3B49112BACD1530B93DE27BF67B952C8C8543619E9C3E6D625BBFEBB797730298C68AC2E33C61369E5,
    y: 01F03F7D772F60A2486A3EC7368B8864A2231DA143A491C950E776DA757992BC48757B642BC83410BFCC13D09C86B3074243A287022BA950E3B63D5D1E562D6FC872
  }
  */
  Uint8List A = curve.encodePoint(publicKeyPoint);
  // A = 43BA28F430CDFF456AE531545F7ECD0AC834A55D9358C0372BFA0C6C6798C0866AEA01EB00742802B8438EA4CB82169C235160627B4C3A9480
}

2.3 Resultado da chave pública

A partir da chave privada é possível gerar a seguinte chave pública: 0x72C86F2D561E5D3DB6E350A92B0287A2434207B3869CD013CCBF1034C82B647B7548BC927975DA76E750C991A443A11D23A264888B36C73E6A48A2602F777D3FF081

3. Assinatura

O algoritmo de assinatura do Ed521 recebe como entrada uma chave privada (66 bytes) e uma mensagem de tamanho arbitrário e produz como saída um par de inteiros R e S. A assinatura é definida no método sign da classe EdPrivateKey.

Para esse exemplo será utilizada a seguinte mensagem: 0x03

A assinatura de uma mensagem é feita da seguinte forma:

3.1. Hash, Chave pública e prefixo

Realize o hash da chave privada utilizando SHAKE256 e recupere os 132 primeiros bytes, o resultado é armazenado em um buffer (h) de 132 bytes, depois construa o escalar privado (s) a partir da primeira metade do resultado do hash e a chave pública (A) como descrito em 2.2.3.. O prefixo é a segunda metade do resultado do hash.

Uint8List sign(Uint8List message) {
  // message = 03
  // privateKey = 2367B29BCEACA04D6FE50D959DEE3D706F3A5F605CE5AAC0205C54CDDA59145C573B932814C7405770CD46171A0EB44C911F8E851ADEEFCC77E5841BF86E0B6486DD

  Uint8List h = this._hashPrivateKey();
  // h = F86EA93928B5EDC2163B90AC3184874865DFDEBD10450817E37EF646EB83E6A03746DDB8F72C863AE672ADE24BF5D3BF2FFB074FD3F5D71BB4D887D80FA560B2C440380F590C13657792D96FA5BF0379507CDD781EB47126F240B015D0C7ADBB297722E1B4D37C106CE7C7E1596708B333AAA9557E1937B3A6E6C7565B8CA9EAD764B506

  Uint8List leftHalf = h.sublist(0, this.curve.keySize);
  // leftHalf = F86EA93928B5EDC2163B90AC3184874865DFDEBD10450817E37EF646EB83E6A03746DDB8F72C863AE672ADE24BF5D3BF2FFB074FD3F5D71BB4D887D80FA560B2C440

  Uint8List s = _getPrivateScalar(leftHalf);
  // s = 00C4B260A50FD887D8B41BD7F5D34F07FB2FBFD3F54BE2AD72E63A862CF7B8DD4637A0E683EB46F67EE317084510BDDEDF6548878431AC903B16C2EDB52839A96EF8

  Point A = _getPublicKeyPoint(s);
  /*
  A = {
    x: 00A7104267A1A89C35214F37DB17948CA9BB9F18671277BEAC3B49112BACD1530B93DE27BF67B952C8C8543619E9C3E6D625BBFEBB797730298C68AC2E33C61369E5,
    y: 01F03F7D772F60A2486A3EC7368B8864A2231DA143A491C950E776DA757992BC48757B642BC83410BFCC13D09C86B3074243A287022BA950E3B63D5D1E562D6FC872
  }
  */

  Uint8List prefix = _getSignaturePrefix(h);
  // prefix = 380F590C13657792D96FA5BF0379507CDD781EB47126F240B015D0C7ADBB297722E1B4D37C106CE7C7E1596708B333AAA9557E1937B3A6E6C7565B8CA9EAD764B506

  ...
}

3.2. Gerar o inteiro secreto (r)

Realize o hash da string "SigEd521" + 0 + 0 ("SigEd521" está em ASCII, o primeiro 0 se dá pelo fato de ser utilizado o Ed521 sem pré hash e o segundo 0 se dá pelo fato de não haver um contexto), do prefixo e da mensagem a ser assinada utilizando o SHAKE256 e recupere os 132 primeiros bytes, o resultado deve ser interpretado como little endian.

Uint8List sign(Uint8List message) {
  ...

  String m = 'SigEd521' + String.fromCharCodes([0x00, 0x00]);
  curveSigner = Uint8List.fromList(m.codeUnits);
  // curveSigner = 393900767670058993385472

  hash.update(curveSigner);
  hash.update(prefix);
  hash.update(message);
  Uint8List r = hash.digest(curve.signatureSize);
  // r = A1A28A471B985043C9AC0743CBEEC1F564995AD6F144C2691056DEFCE6B85FD570F735C8515730A4F24E20ED19C49B530EC7BA14304DFCD6A6AABC52C9994996FC2E3396ADD56BB3290C826D5D9B8247780C8AE34378ECBC58CF2F95C09BA9C95A4EE71AF78B435380577B83400B2040189A70FF5E8DCBEA4221303B25EE5112309DE826

  r = toLittleEndian(r);
  // r = 26E89D301251EE253B302142EACB8D5EFF709A1840200B40837B578053438BF71AE74E5AC9A99BC0952FCF58BCEC7843E38A0C7847829B5D6D820C29B36BD5AD96332EFC964999C952BCAAA6D6FC4D3014BAC70E539BC419ED204EF2A4305751C835F770D55FB8E6FCDE561069C244F1D65A9964F5C1EECB4307ACC94350981B478AA2A1

  ...
}

3.3. Calcular o inteiro R

Primeiro calcule a multiplicação de r pelo ponto gerador da curva, depois realize o encoding desse ponto, o resultado é a parte R da assinatura.

Uint8List sign(Uint8List message) {
  ...

  BigInt rBigInt = decodeBigInt(r);
  rBigInt = rBigInt % order;
  // rBigInt = 492350657D690F6AE3B36994A0F340E54B3556310D0E999585C81A8F17F27C06A4FE94DAEA1A55AF5598EA5D2278C36D5894D9D3031E880529F125AEE1392F18CD
  
  r = encodeBigInt(rBigInt, curve.signatureSize);
  // r = 007C964999C952BCAAA6D6FC4D3014BAC70E539BC419ED204EF2A4305751C835F87FF1F9B1023321AB4188AA8F9DC8B2B49578029B551D0950A0AE4170813DCDC2C2

  Point pointR = generator.mul(r);
  /*
  pointR = {
    x: 010476E7F95E187A0E03B2C1F3710B5953A060FFA1F784A3EA9BF2BAAB4687DC3EBEF403D32BBF0A34A977B3BD4FA44C858D3F2FC70A2AC2EF18A2BCE2FAAF2A6C6F,
    y: 000B88FEE36016F0F4653C262D4C4553EEE3D0012CB26E92C9CBDFB373185651935207ADF5712E06604E92D8D611C81421056C8BCD05583210D3B62FA293FBE3F659
  }
  */

  Uint8List R = curve.encodePoint(pointR);
  // R = 59F6E3FB93A22FB6D310325805CD8B6C052114C811D6D8924E60062E71F5AD07529351561873B3DFCBC9926EB22C01D0E3EE53454C2D263C65F4F01660E3FE880B80

  ...
}

3.4. Calcular o inteiro k

Realize o hash da string "SigEd521" + 0 + 0, do inteiro R, do encoding do ponto da chave pública A e da mensagem a ser assinada utilizando o SHAKE256 e recupere os 132 primeiros bytes, interprete o resultado como little endian (k).

Uint8List sign(Uint8List message) {
  ...
  /*
  A = {
    x: A7104267A1A89C35214F37DB17948CA9BB9F18671277BEAC3B49112BACD1530B93DE27BF67B952C8C8543619E9C3E6D625BBFEBB797730298C68AC2E33C61369E5,
    y: 1F03F7D772F60A2486A3EC7368B8864A2231DA143A491C950E776DA757992BC48757B642BC83410BFCC13D09C86B3074243A287022BA950E3B63D5D1E562D6FC872
  }
  */

  Uint8List eA = curve.encodePoint(A);
  // eA = 72C86F2D561E5D3DB6E350A92B0287A2434207B3869CD013CCBF1034C82B647B7548BC927975DA76E750C991A443A11D23A264888B36C73E6A48A2602F777D3FF081

  String m = 'SigEd521' + String.fromCharCodes([0x00, 0x00]);
  curveSigner = Uint8List.fromList(m.codeUnits);
  // curveSigner = 393900767670058993385472

  hash.update(curveSigner);
  hash.update(R);
  hash.update(eA);
  hash.update(message);
  Uint8List k = hash.digest(curve.signatureSize);
  // k = FFC0888CBC9D36737C7DED5FF22A5BFBB80CFEC1D1E24E5043DD5C8E17FAD80A77ACDE025B50920F47AB243F67A804DCD335053E4905CA7FF6D25F32A85CD6A3E1BAA5278A6AA332FF6669D5CE2AFB8B983E8227195AFEE0471889393153411947D58F0EC9ABA8AEEC2613FC29CA2A73F5C2EFF1B6F16C65861CFDF529B53E07D1083471

  k = toLittleEndian(k);  
  // k = 713408D1073EB529F5FD1C86656CF1B6F1EFC2F5732ACA29FC1326ECAEA8ABC90E8FD5471941533139891847E0FE5A1927823E988BFB2ACED56966FF32A36A8A27A5BAE1A3D65CA8325FD2F67FCA05493E0535D3DC04A8673F24AB470F92505B02DEAC770AD8FA178E5CDD43504EE2D1C1FE0CB8FB5B2AF25FED7D7C73369DBC8C88C0FF

  ...
}

3.5. Calcular o inteiro S e retornar a assinatura

Calcule S = (r + k * s) mod L, onde L é a ordem da curva. Para eficiência, reduza k módulo L primeiro. Após isso, a assinatura é a concatenação de R com a interpretação little endian de S.

Uint8List sign(Uint8List message) {
  ...

  BigInt reducedK = decodeBigInt(k) % order;
  // reducedK = 5E957D1EE76C185C99CB9BE712822D600B59210CDC743460EFB194948D14BD4451407FB48A1A263BE565F5855FAE010F068B193E86544F5B2052333A748848FA25

  Uint8List S = encodeBigInt(
      (decodeBigInt(r) + (reducedK * decodeBigInt(s))) % order,
      curve.keySize);
  /*
  reducedK * s = 48AC5B71B3CBAA2ACF3C1C9256ED46F6F72CDA6B68F9D3D2893C9F07DD76DAA9E95CAD625038BEC31BC721EB883DEDFE9BF95A527DD1CA469414E1C4080019E3117209C324C7BE3D8E2DD5C0D1C74CF38F5CF80FE851D45AE431E1FBCFB2FA6E13041815714017F6122AEE637B747EE946A40044288CB6E6DC55B0892A1AC19B39D8

  r + reducedK * s = 48AC5B71B3CBAA2ACF3C1C9256ED46F6F72CDA6B68F9D3D2893C9F07DD76DAA9E95CAD625038BEC31BC721EB883DEDFE9BF95A527DD1CA469414E1C4080019E311BB2D138A45274CF911892A6668403474A82D66195EE2F479B7AA165ECAECEA19A916AA4C2A324BC180874DD896F7ACB3FC951DFB8FD56EE17FA1AED8FBFACA52A5

  S = (r + reducedK * s) mod order = 3AC007F81C4434D2D1AFADA41AB28A34F30F2A3D95F199E7052947A6E0F70A9FC1364315BFECDC04BC36DF27E0B2AFE1CECBE1AAC0BC0D5E
  */

  S = toLittleEndian(S);
  // S = 1B23A076975FCBB327E756A3A486E791745BF5120E69C4D908CA3F04BEA5E958774BAB073A8DD7B7D47737CEE5FC3F4D7D7881B13A7540AF8E40BE02722D5DEBC4

  return Uint8List.fromList(R + S);
}

3.6. Resultado da assinatura

A partir da chave privada e da mensagem é possível gerar a seguinte assinatura: 59F6E3FB93A22FB6D310325805CD8B6C052114C811D6D8924E60062E71F5AD07529351561873B3DFCBC9926EB22C01D0E3EE53454C2D263C65F4F01660E3FE880B80C4EB5D2D7202BE408EAF40753AB181787D4D3FFCE5CE3777D4B7D78D3A07AB4B7758E9A5BE043FCA08D9C4690E12F55B7491E786A4A356E727B3CB5F9776A0231B00

4. Verificação

A verificação do Ed521 recebe como parâmetro a chave pública (66 bytes), uma mensagem de tamanho arbitrário e uma assinatura {R, S} (132 bytes) e produz como saída um booleano da verificação se a assinatura é válida ou não para a mensagem. A verificação é definida no método verify da classe EdPublicKey.

A verificação é feita da seguinte forma:

4.1. Decoding da assinatura e da chave pública

Separe a assinatura em duas metades de 66 bytes, sendo a primeira metade o R e a segunda o S. Após a divisão da assinatura, decodifique o R e a chave pública (A) como pontos. O método para realizar esse procedimento é o decodePoint definido na classe TwistedEdwardCurve.

// decode R
Point decodePoint(Uint8List encodedPoint) {
  // encodedPoint = 59F6E3FB93A22FB6D310325805CD8B6C052114C811D6D8924E60062E71F5AD07529351561873B3DFCBC9926EB22C01D0E3EE53454C2D263C65F4F01660E3FE880B80

  Uint8List eP = Uint8List.fromList(encodedPoint); // object copy
  int size = eP.length;
  // size = 66

  int sign = eP[size - 1] & 0x80;
  // sign = 132

  eP[size - 1] &= ~0x80;
  // eP = 59F6E3FB93A22FB6D310325805CD8B6C052114C811D6D8924E60062E71F5AD07529351561873B3DFCBC9926EB22C01D0E3EE53454C2D263C65F4F01660E3FE880B00

  BigInt y = decodeBigInt(toLittleEndian(eP));
  // y = B88FEE36016F0F4653C262D4C4553EEE3D0012CB26E92C9CBDFB373185651935207ADF5712E06604E92D8D611C81421056C8BCD05583210D3B62FA293FBE3F659

  BigInt x = _xRecover(y, sign);
  // x = 10476E7F95E187A0E03B2C1F3710B5953A060FFA1F784A3EA9BF2BAAB4687DC3EBEF403D32BBF0A34A977B3BD4FA44C858D3F2FC70A2AC2EF18A2BCE2FAAF2A6C6F

  return Point(x, y, this);
}

// decode A
Point decodePoint(Uint8List encodedPoint) {
  // encodedPoint = 72C86F2D561E5D3DB6E350A92B0287A2434207B3869CD013CCBF1034C82B647B7548BC927975DA76E750C991A443A11D23A264888B36C73E6A48A2602F777D3FF081

  Uint8List eP = Uint8List.fromList(encodedPoint); // object copy
  int size = eP.length;
  // size = 66

  int sign = eP[size - 1] & 0x80;
  // sign = 132

  eP[size - 1] &= ~0x80;
  // eP = 72C86F2D561E5D3DB6E350A92B0287A2434207B3869CD013CCBF1034C82B647B7548BC927975DA76E750C991A443A11D23A264888B36C73E6A48A2602F777D3FF001

  BigInt y = decodeBigInt(toLittleEndian(eP));
  // y = 1F03F7D772F60A2486A3EC7368B8864A2231DA143A491C950E776DA757992BC48757B642BC83410BFCC13D09C86B3074243A287022BA950E3B63D5D1E562D6FC872

  BigInt x = _xRecover(y, sign);
  // x = A7104267A1A89C35214F37DB17948CA9BB9F18671277BEAC3B49112BACD1530B93DE27BF67B952C8C8543619E9C3E6D625BBFEBB797730298C68AC2E33C61369E5

  return Point(x, y, this);
}

bool verify(Uint8List message, Uint8List signature) {
  Curve curve = this.curve;
  BigInt order = curve.order;
  int signatureSize = curve.signatureSize;
  Uint8List A = this.publicKey;

  Uint8List R = signature.sublist(0, signatureSize >> 1);
  // R = 59F6E3FB93A22FB6D310325805CD8B6C052114C811D6D8924E60062E71F5AD07529351561873B3DFCBC9926EB22C01D0E3EE53454C2D263C65F4F01660E3FE880B80

  Uint8List S = toLittleEndian(signature.sublist(signatureSize >> 1));
  // S = 1B23A076975FCBB327E756A3A486E791745BF5120E69C4D908CA3F04BEA5E958774BAB073A8DD7B7D47737CEE5FC3F4D7D7881B13A7540AF8E40BE02722D5DEBC4
  
  Point pointR = curve.decodePoint(R);
  /* 
  pointR = {
    x: 10476E7F95E187A0E03B2C1F3710B5953A060FFA1F784A3EA9BF2BAAB4687DC3EBEF403D32BBF0A34A977B3BD4FA44C858D3F2FC70A2AC2EF18A2BCE2FAAF2A6C6F,
    y: B88FEE36016F0F4653C262D4C4553EEE3D0012CB26E92C9CBDFB373185651935207ADF5712E06604E92D8D611C81421056C8BCD05583210D3B62FA293FBE3F659,
  }
  */

  Point pointA = curve.decodePoint(A);
  /* 
  pointA = {
    x: A7104267A1A89C35214F37DB17948CA9BB9F18671277BEAC3B49112BACD1530B93DE27BF67B952C8C8543619E9C3E6D625BBFEBB797730298C68AC2E33C61369E5,
    y: 1F03F7D772F60A2486A3EC7368B8864A2231DA143A491C950E776DA757992BC48757B642BC83410BFCC13D09C86B3074243A287022BA950E3B63D5D1E562D6FC872,
  }
  */
  ...
}

4.2. Calculando k

Realize o hash da string "SigEd521" + 0 + 0, do R, da chave pública e da mensagem a ser assinada utilizando o SHAKE256 e recupere os 132 primeiros bytes, interprete em little endian o resultado como um inteiro k.

bool verify(Uint8List message, Uint8List signature) {
  ...

  String m = 'SigEd521' + String.fromCharCodes([0x00, 0x00]);
  curveSigner = Uint8List.fromList(m.codeUnits);
  // curveSigner = 393900767670058993385472

  hash.update(curveSigner);
  hash.update(R);
  hash.update(A);
  hash.update(message);

  Uint8List k = hash.digest(signatureSize);
  // k = FFC0888CBC9D36737C7DED5FF22A5BFBB80CFEC1D1E24E5043DD5C8E17FAD80A77ACDE025B50920F47AB243F67A804DCD335053E4905CA7FF6D25F32A85CD6A3E1BAA5278A6AA332FF6669D5CE2AFB8B983E8227195AFEE0471889393153411947D58F0EC9ABA8AEEC2613FC29CA2A73F5C2EFF1B6F16C65861CFDF529B53E07D1083471

  k = toLittleEndian(k);
  // k = 713408D1073EB529F5FD1C86656CF1B6F1EFC2F5732ACA29FC1326ECAEA8ABC90E8FD5471941533139891847E0FE5A1927823E988BFB2ACED56966FF32A36A8A27A5BAE1A3D65CA8325FD2F67FCA05493E0535D3DC04A8673F24AB470F92505B02DEAC770AD8FA178E5CDD43504EE2D1C1FE0CB8FB5B2AF25FED7D7C73369DBC8C88C0FF
  ...
}

4.3. Verificando o grupo da equação [S]Generator = pointR + [k]pointA

Para validar a assinatura, é preciso verificar se S vezes o ponto gerador é igual à soma do ponto R com a multiplicação de k com o ponto A.

bool verify(Uint8List message, Uint8List signature) {
  ...

  BigInt reducedK = decodeBigInt(k) % order;
  k = encodeBigInt(reducedK, curve.keySize);
  // k = 5E957D1EE76C185C99CB9BE712822D600B59210CDC743460EFB194948D14BD4451407FB48A1A263BE565F5855FAE010F068B193E86544F5B2052333A748848FA25

  Point left = pointA.mul(k).add(pointR); // (pointA * k) + pointR
  /*
  left = {
    x: 107DF403D2FF3040955DD1D746444571EEB1353DFE5260696FDCFA805D10CCC06A0CA10F0BCD9E966B4829B8AB78DA31DA7891238361AFBA1248FE8B6952131D55C,
    y: BB8CC0A41EA8D62B3E0E54A63A92C6F94E53F9DED3E45DBCAA2D4C87B2BB91220E0012ED78F59CB6342392F16CB9C68EC23AF847FD62B76F7441CAB31E5DE1C76
  }
  */

  Point right = curve.generator.mul(S); // generator * S
  /*
  right = {
    x: 107DF403D2FF3040955DD1D746444571EEB1353DFE5260696FDCFA805D10CCC06A0CA10F0BCD9E966B4829B8AB78DA31DA7891238361AFBA1248FE8B6952131D55C,
    y: BB8CC0A41EA8D62B3E0E54A63A92C6F94E53F9DED3E45DBCAA2D4C87B2BB91220E0012ED78F59CB6342392F16CB9C68EC23AF847FD62B76F7441CAB31E5DE1C76
  }
  */

  return left == right;
}