From c21ef9ef81ca98e7fb943e31078995793113c829 Mon Sep 17 00:00:00 2001 From: Evgeniy Kulikov Date: Thu, 17 Oct 2019 16:11:58 +0300 Subject: [PATCH] initial --- LICENSE | 201 +++++++++++++++++++++++++++++++++++++++++ README.md | 62 +++++++++++++ ecdsa.go | 221 ++++++++++++++++++++++++++++++++++++++++++++++ ecdsa_test.go | 179 +++++++++++++++++++++++++++++++++++++ go.mod | 10 +++ go.sum | 17 ++++ internal/error.go | 7 ++ rfc6979.go | 61 +++++++++++++ rfc6979_test.go | 85 ++++++++++++++++++ test/keys.go | 136 ++++++++++++++++++++++++++++ wif.go | 62 +++++++++++++ wif_test.go | 144 ++++++++++++++++++++++++++++++ 12 files changed, 1185 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 ecdsa.go create mode 100644 ecdsa_test.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/error.go create mode 100644 rfc6979.go create mode 100644 rfc6979_test.go create mode 100644 test/keys.go create mode 100644 wif.go create mode 100644 wif_test.go diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..2fa03b6 --- /dev/null +++ b/README.md @@ -0,0 +1,62 @@ +# NeoFS Crypto library + +**This package contains useful methods to work with crypto-primitives, that used in NeoFS / NeoBlockchain.** + +## Examples + +### Simple Marshal / Unmarshal ECDSA public key (PK): + +``` +// returns slice of 33 bytes marshaled public key +data := crypto.MarshalPublicKey(&sk.PublicKey) + +// returns public key decoded from 33 bytes +pk := crypto.UnmarshalPublicKey(data) +``` + +### Simple Marshal / Unmarshal ECDSA private key (SK): + +``` +// returns slice of 32 bytes marshaled private key +data := crypto.MarshalPrivateKey(&sk) + +// returns private key decoded from 32 bytes or error, +// if something whet wrong +newSk, err := crypto.UnmarshalPrivateKey(data) +``` + +### ECDSA Sign / Verify bytes using PK / SK + +``` +// Sign returns signature (slice of 65 bytes) of SK for passed message (slice of bytes), +// or error, if something went wrong: +signature, err := crypto.Sign(sk, message) + +// Verify returns error message if PK is empty or +// passed wrong signature (slice of 65 bytes) for message (slice of bytes), +err := crypto.Verify(&sk.PublicKey, signature, message) +``` + +### RFC6979 Sign / Verify bytes using PK / SK + +``` +// Sign returns signature (slice of 65 bytes) of SK for passed message (slice of bytes), +// or error, if something went wrong: +signature, err := crypto.SignRFC6979(sk, message) + +// Verify returns error message if PK is empty or +// passed wrong signature (slice of 65 bytes) for message (slice of bytes), +err := crypto.VerifyRFC6979(&sk.PublicKey, signature, message) +``` + +### WIF Encode / Decode private key (SK) + +``` +// WIFEncode encodes the given private key into a WIF string. +// if sk or sk.D is empty, returns error +wif, err := crypto.WIFEncode(sk) + +// WIFDecode decoded the given WIF string into a private key. +// if something went wrong, returns error: +skFromWIF, err := crypto.WIFDecode(wif) +``` diff --git a/ecdsa.go b/ecdsa.go new file mode 100644 index 0000000..ee6ada1 --- /dev/null +++ b/ecdsa.go @@ -0,0 +1,221 @@ +package crypto + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/sha256" + "crypto/x509" + "math/big" + + "github.com/nspcc-dev/neofs-crypto/internal" + "github.com/pkg/errors" +) + +const ( + // ErrEmptyPublicKey when PK passed to Verify method is nil + ErrEmptyPublicKey = internal.Error("empty public key") + + // ErrInvalidSignature when signature passed to Verify method is mismatch + ErrInvalidSignature = internal.Error("invalid signature") + + // ErrCannotUnmarshal when signature ([]byte) passed to Verify method has wrong format + // and cannot be parsed. + ErrCannotUnmarshal = internal.Error("could not unmarshal signature") + + // PrivateKeyCompressedSize is constant with compressed size of private key (SK). + // D coordinate stored, recover PK by formula x, y = curve.ScalarBaseMul(d,bytes). + PrivateKeyCompressedSize = 32 + + // PublicKeyCompressedSize is constant with compressed size of public key (PK). + PublicKeyCompressedSize = 33 + + // PublicKeyUncompressedSize is constant with uncompressed size of public key (PK). + // First byte always should be 0x4 other 64 bytes is X and Y (32 bytes per coordinate). + // 2 * 32 + 1. + PublicKeyUncompressedSize = 65 +) + +// P256 is base elliptic curve +var curve = elliptic.P256() + +// Marshal converts a points into the uncompressed form specified in section 4.3.6 of ANSI X9.62. +func marshalXY(x, y *big.Int) []byte { + return elliptic.Marshal(curve, x, y) +} + +// unmarshalXY converts a point, serialized by Marshal, into an x, y pair. +// It is an error if the point is not in uncompressed form. +// On error, x,y = nil. +// Unlike the original version of the code, we ignore that x or y not on the curve +// -------------- +// It's copy-paste elliptic.Unmarshal(curve, data) stdlib function, without last line +// of code 🤔 +// Link - https://golang.org/pkg/crypto/elliptic/#Unmarshal +func unmarshalXY(data []byte) (x *big.Int, y *big.Int) { + if len(data) != PublicKeyUncompressedSize { + return + } else if data[0] != 4 { // uncompressed form + return + } + + p := curve.Params().P + x = new(big.Int).SetBytes(data[1:PublicKeyCompressedSize]) + y = new(big.Int).SetBytes(data[PublicKeyCompressedSize:]) + + if x.Cmp(p) >= 0 || y.Cmp(p) >= 0 { + x, y = nil, nil + } + + return +} + +// decompressPoints using formula y^2 = x^3 + ax + b mod p +func decompressPoints(x *big.Int, yBit uint) (*big.Int, *big.Int) { + params := curve.Params() + + // x^3 mod P + x3 := new(big.Int).Exp(x, new(big.Int).SetInt64(3), params.P) + + // a * x mod P + ax := new(big.Int).Mul(x, new(big.Int).SetInt64(-3)) + ax.Mod(ax, params.P) + + // x^3 + a * x mod P + x3.Add(x3, ax) + x3.Mod(x3, params.P) + + // x^3 + a * x + b mod P + x3.Add(x3, params.B) + x3.Mod(x3, params.P) + + // y = sqrt(x^3 + ax + b mod p) mod P + y := new(big.Int).ModSqrt(x3, params.P) + + // big.Int.Jacobi(a, b) can return nil + if y == nil { + return nil, nil + } + + if y.Bit(0) != (yBit & 0x1) { + y.Neg(y) + y.Mod(y, params.P) + } + + return x, y +} + +func encodePoint(x, y *big.Int) []byte { + data := make([]byte, PublicKeyCompressedSize) + copy(data[1:], x.Bytes()) + + if y.Bit(0) == 0x1 { + data[0] = 0x3 + } else { + data[0] = 0x2 + } + + return data +} + +func decodePoint(data []byte) (*big.Int, *big.Int) { + // empty data + if len(data) == 0 { + return nil, nil + } + + switch prefix := data[0]; prefix { + case 0x02, 0x03: // compressed key + // Incorrect length for compressed encoding + if len(data) != PublicKeyCompressedSize { + return nil, nil + } + + return decompressPoints(new(big.Int).SetBytes(data[1:]), uint(prefix)) + case 0x04: // uncompressed key + // To get the public key, besides getting it from the data and checking, + // we also must to check that the points are on an elliptic curve + return unmarshalXY(data) + } + + // unknown type + return nil, nil +} + +// MarshalPublicKey to bytes +func MarshalPublicKey(key *ecdsa.PublicKey) []byte { + if key == nil || key.X == nil || key.Y == nil { + return nil + } + + return encodePoint(key.X, key.Y) +} + +// UnmarshalPublicKey from bytes +func UnmarshalPublicKey(data []byte) *ecdsa.PublicKey { + if x, y := decodePoint(data); x != nil && y != nil && curve.IsOnCurve(x, y) { + return &ecdsa.PublicKey{ + Curve: curve, + X: x, + Y: y, + } + } + + return nil +} + +// UnmarshalPrivateKey method to parse SK from bytes. +// It is similar to `ecdsa.Generate()` but uses pre-defined big.Int and curve for NEO Blockchain +// Link - https://golang.org/pkg/crypto/ecdsa/#GenerateKey +func UnmarshalPrivateKey(data []byte) (*ecdsa.PrivateKey, error) { + if len(data) == PrivateKeyCompressedSize { // todo: consider using only NEO blockchain private keys + d := new(big.Int).SetBytes(data) + priv := new(ecdsa.PrivateKey) + priv.PublicKey.Curve = curve + priv.D = d + priv.PublicKey.X, priv.PublicKey.Y = curve.ScalarBaseMult(data) + + return priv, nil + } + + return x509.ParseECPrivateKey(data) +} + +// MarshalPrivateKey to bytes +func MarshalPrivateKey(key *ecdsa.PrivateKey) []byte { + return key.D.Bytes() +} + +// hashBytes returns the sha256 sum. +func hashBytes(data []byte) []byte { + buf := sha256.Sum256(data) + return buf[:] +} + +// Verify verifies the signature in r, s of hash using the public key, pub. Its +// return value records whether the signature is valid. +func Verify(pub *ecdsa.PublicKey, hash, data []byte) error { + if r, s := unmarshalXY(hash); r == nil || s == nil { + // panic("could not unmarshal r / s") + return ErrCannotUnmarshal + } else if pub == nil { + return ErrEmptyPublicKey + } else if !ecdsa.Verify(pub, hashBytes(data), r, s) { + return errors.Wrapf(ErrInvalidSignature, "%0x : %0x", r, s) + } + + return nil +} + +// Sign signs a data using the private key. If the data is longer than +// the bit-length of the private key's curve order, the hash will be +// truncated to that length. It returns the signature as slice bytes. +// The security of the private key depends on the entropy of rand. +func Sign(key *ecdsa.PrivateKey, data []byte) ([]byte, error) { + x, y, err := ecdsa.Sign(rand.Reader, key, hashBytes(data)) + if err != nil { + return nil, err + } + + return marshalXY(x, y), nil +} diff --git a/ecdsa_test.go b/ecdsa_test.go new file mode 100644 index 0000000..dae0b60 --- /dev/null +++ b/ecdsa_test.go @@ -0,0 +1,179 @@ +package crypto + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "encoding/hex" + "math/big" + "strconv" + "testing" + + "github.com/nspcc-dev/neofs-crypto/test" + "github.com/stretchr/testify/require" +) + +var testKeys = [...]string{ + "0375099c302b77664a2508bec1cae47903857b762c62713f190e8d99912ef76737", + "025188d33a3113ac77fea0c17137e434d704283c234400b9b70bcdf4829094374a", + "02c4c574d1bbe7efb2feaeed99e6c03924d6d3c9ad76530437d75c07bff3ddcc0f", + "02563eece0b9035e679d28e2d548072773c43ce44a53cb7f30d3597052210dbb70", + "02f8152966ad33b3c2622bdd032f5989fbd63a9a3af34e12eefee912c37defc880", + "0252d9fd2376f6b3bcb4706cad54ec031d95a1a70414129286c247cd2bc521f73f", + "0251ec65b2496b1d8ece3efe68a8b57ce7bc75b4171f07fa5b26c63a27fb4f9216", + "025f7d63e18e6b896730f45989b7a8d00c0b86c75c2b834d903bc681833592bdcc", + "02d351a4c87ec3b33e62610cb3fd197962c0081bbe1b1b888bc41844f4c6df9cd3", + "036e3859e6ab43c0f45b7891761f0da86a7b62f931f3d963efd3103924920a73b3", + "03c02a93134f98d9c78ec54b1b1f97fc64cd81360f53a293f41e4ad54aac3c5717", + "03fea219d4ccfd7641cebbb2439740bb4bd7c4730c1abd6ca1dc44386533816df9", + "02a33413277a319cc6fd4c54a2feb9032eba668ec587f307e319dc48733087fa61", +} + +func Test_decompressPoints(t *testing.T) { + t.Run("prepared public keys: decompressPoints", func(t *testing.T) { + for i := range testKeys { + bytes, err := hex.DecodeString(testKeys[i]) + require.NoErrorf(t, err, testKeys[i]) + + x, y := decompressPoints(new(big.Int).SetBytes(bytes[1:]), uint(bytes[0])) + require.NotNil(t, x) + require.NotNil(t, y) + + res := MarshalPublicKey(&ecdsa.PublicKey{Curve: curve, X: x, Y: y}) + require.Equal(t, testKeys[i], hex.EncodeToString(res)) + } + }) +} + +func TestMarshalUnmarshal(t *testing.T) { + t.Run("prepared public keys: unmarshal / marshal", func(t *testing.T) { + for i := range testKeys { + bytes, err := hex.DecodeString(testKeys[i]) + require.NoErrorf(t, err, testKeys[i]) + + key := UnmarshalPublicKey(bytes) + res := MarshalPublicKey(key) + require.Equal(t, testKeys[i], hex.EncodeToString(res)) + } + }) + + t.Run("try to use stored keys", func(t *testing.T) { + for i := 0; i < 10; i++ { + i := i + sk := test.DecodeKey(i) + + t.Run("marshal key_"+strconv.Itoa(i), func(t *testing.T) { + bytes := MarshalPublicKey(&sk.PublicKey) + require.Equal(t, testKeys[i], hex.EncodeToString(bytes)) + }) + + t.Run("unmarshal key_"+strconv.Itoa(i), func(t *testing.T) { + bytes, err := hex.DecodeString(testKeys[i]) + require.NoError(t, err) + + pk := UnmarshalPublicKey(bytes) + require.Equal(t, pk, &sk.PublicKey) + }) + } + }) + + t.Run("prepared private key: unmarshal / marshal", func(t *testing.T) { + walletPublicKey := "03fb2e07eba5477dd7de3f627d8803eb86bc36b98132f51d81017343bb6585d9c7" + walletPrivateKey := "dc80f6900b7b7a791496c06aa77695d744fafaaeff5c143801eb97935448c671" + + data, err := hex.DecodeString(walletPrivateKey) + require.NoError(t, err) + + _, err = x509.ParseECPrivateKey(data) + require.Error(t, err) + + privateKey, err := UnmarshalPrivateKey(data) + require.NoError(t, err) + + require.Equal(t, hex.EncodeToString(MarshalPublicKey(&privateKey.PublicKey)), walletPublicKey) + require.Equal(t, data, MarshalPrivateKey(privateKey)) + }) +} + +func TestSignVerify(t *testing.T) { + t.Run("should not fail when we pass bad data to unmarshalXY", func(t *testing.T) { + var ( + data = []byte("Hello world") + key = test.DecodeKey(0) + ) + + require.NotPanics(t, func() { + { // 1. simple example + UnmarshalPublicKey(nil) + } + + { // 2. pass bad data (not public key) + r1, s1, err := ecdsa.Sign(rand.Reader, key, hashBytes(data)) + require.NoError(t, err) + + hash := marshalXY(r1, s1) + UnmarshalPublicKey(hash) + } + + { // 3. bad big.Ints + hash := marshalXY(big.NewInt(0), big.NewInt(1)) + UnmarshalPublicKey(hash) + } + }) + }) + + t.Run("low level", func(t *testing.T) { + var ( + data = []byte("Hello world") + curve = elliptic.P256() + key = test.DecodeKey(0) + ) + + r1, s1, err := ecdsa.Sign(rand.Reader, key, + hashBytes(data)) + require.NoError(t, err) + + hash := marshalXY(r1, s1) + + { // This is just to validate, that we are on right way.. try to unmarshal R/S from hash + // validate bytes length + byteLen := (curve.Params().BitSize + 7) >> 3 + require.Len(t, hash, 1+2*byteLen) + + // uncompressed form? + require.Equal(t, byte(4), hash[0]) + + // validate R / S + p := curve.Params().P + r := new(big.Int).SetBytes(hash[1 : 1+byteLen]) + s := new(big.Int).SetBytes(hash[1+byteLen:]) + require.True(t, r.Cmp(p) < 0) + require.True(t, s.Cmp(p) < 0) + + // don't check curves + // require.True(t, curve.IsOnCurve(r, s)) + } + + r2, s2 := unmarshalXY(hash) + require.NotNil(t, r2) + require.NotNil(t, s2) + + require.Equal(t, r1.Bytes(), r2.Bytes()) + require.Equal(t, s2.Bytes(), s2.Bytes()) + }) + + t.Run("high level", func(t *testing.T) { + var ( + data = []byte("Hello world") + key = test.DecodeKey(0) + ) + + hash, err := Sign(key, data) + require.NoError(t, err) + + for i := 0; i < 100; i++ { + require.NoError(t, Verify(&key.PublicKey, hash, data)) + } + }) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..21d3bdc --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module github.com/nspcc-dev/neofs-crypto + +go 1.13 + +require ( + github.com/mr-tron/base58 v1.1.2 + github.com/nspcc-dev/rfc6979 v0.1.0 + github.com/pkg/errors v0.8.1 + github.com/stretchr/testify v1.4.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..ed5feea --- /dev/null +++ b/go.sum @@ -0,0 +1,17 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/mr-tron/base58 v1.1.2 h1:ZEw4I2EgPKDJ2iEw0cNmLB3ROrEmkOtXIkaG7wZg+78= +github.com/mr-tron/base58 v1.1.2/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/nspcc-dev/rfc6979 v0.1.0 h1:Lwg7esRRoyK1Up/IN1vAef1EmvrBeMHeeEkek2fAJ6c= +github.com/nspcc-dev/rfc6979 v0.1.0/go.mod h1:exhIh1PdpDC5vQmyEsGvc4YDM/lyQp/452QxGq/UEso= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/internal/error.go b/internal/error.go new file mode 100644 index 0000000..c408614 --- /dev/null +++ b/internal/error.go @@ -0,0 +1,7 @@ +package internal + +// Error is a custom error +type Error string + +// Error is an implementation of error interface +func (e Error) Error() string { return string(e) } diff --git a/rfc6979.go b/rfc6979.go new file mode 100644 index 0000000..fe85687 --- /dev/null +++ b/rfc6979.go @@ -0,0 +1,61 @@ +package crypto + +import ( + "crypto/ecdsa" + "crypto/sha256" + "math/big" + + "github.com/nspcc-dev/neofs-crypto/internal" + "github.com/nspcc-dev/rfc6979" + "github.com/pkg/errors" +) + +const ( + // RFC6979SignatureSize contains r and s coordinates (32 bytes) + RFC6979SignatureSize = 64 + + // ErrWrongHashSize when passed signature to VerifyRFC6979 has wrong size + ErrWrongHashSize = internal.Error("wrong hash size") + + // ErrWrongSignature when passed signature to VerifyRFC6979 isn't valid + ErrWrongSignature = internal.Error("wrong signature") +) + +// SignRFC6979 signs an arbitrary length hash (which should be the result of +// hashing a larger message) using the private key. It returns the +// signature as a pair of integers. +// +// Note that FIPS 186-3 section 4.6 specifies that the hash should be truncated +// to the byte-length of the subgroup. This function does not perform that +func SignRFC6979(key *ecdsa.PrivateKey, msg []byte) ([]byte, error) { + msgHash := sha256.Sum256(msg) + + r, s, err := rfc6979.SignECDSA(key, msgHash[:], sha256.New) + if err != nil { + return nil, err + } + + return append(r.Bytes(), s.Bytes()...), nil +} + +func decodeSignature(sig []byte) (*big.Int, *big.Int, error) { + if ln := len(sig); ln != RFC6979SignatureSize { + return nil, nil, errors.Wrapf(ErrWrongHashSize, "actual=%d, expect=%d", ln, RFC6979SignatureSize) + } + + return new(big.Int).SetBytes(sig[:32]), new(big.Int).SetBytes(sig[32:]), nil +} + +// VerifyRFC6979 verifies the signature in r, s of hash using the public key, pub. Its +// return value records whether the signature is valid. +func VerifyRFC6979(key *ecdsa.PublicKey, hash, data []byte) error { + msgHash := sha256.Sum256(data) + + if r, s, err := decodeSignature(hash); err != nil { + return err + } else if !ecdsa.Verify(key, msgHash[:], r, s) { + return ErrWrongSignature + } + + return nil +} diff --git a/rfc6979_test.go b/rfc6979_test.go new file mode 100644 index 0000000..cb5e22b --- /dev/null +++ b/rfc6979_test.go @@ -0,0 +1,85 @@ +package crypto + +import ( + "encoding/hex" + "testing" + + "github.com/stretchr/testify/require" +) + +var ( + rfc6979Data = "1f2155c0e513a7dab93d8b468809cd30a03c62326ec051deed031a6c6fbbdf" + + "02ca351745fa86b9ba5a9452d785ac4f7fc2b7548ca2a46c4fcf4a0000000053724e000000000000001e61" + + rfc6979Signatures = "03012d47e76210aec73be39ab3d186e0a40fe8d86bfa3d4fabfda57ba13b88f96a" + + "e6e87810285bdcba714b5c7a38ca32a3759f3b6d10b3ae69a3e33a444dd7a0f5025a730e07a81170e89124a8115d10bf29446b4a09280e567fca4f27a6d0fcd8" + + "e6e87810285bdcba714b5c7a38ca32a3759f3b6d10b3ae69a3e33a444dd7a0f5025a730e07a81170e89124a8115d10bf29446b4a09280e567fca4f27a6d0fcd8" + + "02f7528ce97c9a93558efe7d4f62577aabdf771c931f54a71be6ad21e7d9cc1777" + + "63efa21e48c3f56f9c8b1c7b24fa8141a4dd33cc12ae5d0bc2c885c628b2eb5b96aa1d7514e343c2221a49a8664415f1944452560bab13ff14ff3c6549da0665" + + "63efa21e48c3f56f9c8b1c7b24fa8141a4dd33cc12ae5d0bc2c885c628b2eb5b6955e289eb1cbc3edde5b65799bbea0e28a2a8579b6c8a85deba8e5db2891eec" + + "023e00f03a16e8707ce045eb42ee80d392451541ee510dc18e1c8befbac54d7426" + + "314e2f2adaaf67908aba6cb1183376574f27c1cafad5d8d852169f5adb26a6ac52641096d9051cdcd91e02420a0643827af535f25d8a9dafe4c5a00554a412ed" + + "314e2f2adaaf67908aba6cb1183376574f27c1cafad5d8d852169f5adb26a6ac52641096d9051cdcd91e02420a0643827af535f25d8a9dafe4c5a00554a412ed" + + "0340750b92789821683283bcb98e32b7e032b94f267b6964613fc31a7ce5813fdd" + + "9e155d31ba033d0637b03d9820c9312709376ab5d7f0a3b08392e47a5807bb76182f7add86b60723e120b25677626f3ab78fe04ed00ae0bb86503d8312c6d0bd" + + "9e155d31ba033d0637b03d9820c9312709376ab5d7f0a3b08392e47a5807bb76182f7add86b60723e120b25677626f3ab78fe04ed00ae0bb86503d8312c6d0bd" + + "03fef27f744e829131d0ec980829fafa51db1714c2761d9f78762c008c323e9d66" + + "f2de56e0777506fa52c19166706856794152a65e75a29dc6fc8a011d2abe1233c8e5e807a3d6b932e92ff91338755136446c57c9ec27df7eec04165e8ae38536" + + "f2de56e0777506fa52c19166706856794152a65e75a29dc6fc8a011d2abe1233371a17f75c2946ce16d006ecc78aaec9787aa2e3baefbf0607b5b464717fa01b" + + "0368a6f5829fb2a34fa03d0308ae6b05f433f2904d9a852fed1f5d2eb598ca7947" + + "9595d09ad84355e2c19ca5f08c19b9492816add735c7e01e549902f04b2c9d1ad7ea131a5e19ae325fffe5a4027383ff2132815c633abf318c2dc170890afc52" + + "9595d09ad84355e2c19ca5f08c19b9492816add735c7e01e549902f04b2c9d1a2815ece4a1e651cea0001a5bfd8c7c009bb4795143dcdf53678c0952735828ff" + + keys = []string{ + "L3o221BojgcCPYgdbXsm6jn7ayTZ72xwREvBHXKknR8VJ3G4WmjB", + "KxLBJgapF8FucHptTm49BaW7wMmQGq97mdeYSnyDmVd39SCFxjBc", + "Kyf6PExBaDHbYwmV7HAwaiqJM2x7wBqLHZrZqdKvF2RQsmDRfXgG", + "KwMWwRYN72vFFV2pjTv9XrWz3tKCvANZQ5BHSKkGCBS5UdKeGTEG", + "L4b9sjSbxfwJPUX9ESPV3LAU9PwLwrAuthVEpGc43Pm9ELtpky3b", + "KyCQQ1LHNYsUQ2ZkD6yQry3hnMUWMvrbk9dHKBwq9ogGXippznKs", + } +) + +func TestRFC6979(t *testing.T) { + body, err := hex.DecodeString(rfc6979Data) + require.NoError(t, err) + + data, err := hex.DecodeString(rfc6979Signatures) + require.NoError(t, err) + + var offset int + + for i := 0; i < 6; i++ { + key, err := WIFDecode(keys[i]) + require.NoError(t, err) + + pub := UnmarshalPublicKey(data[offset : offset+PublicKeyCompressedSize]) + offset += PublicKeyCompressedSize + + require.NotNilf(t, pub, "step: %d", i) + require.Equal(t, &key.PublicKey, pub) + + { // Generated by Go + sig := data[offset : offset+RFC6979SignatureSize] + + res, err := SignRFC6979(key, body) + require.NoError(t, err) + + require.Equal(t, sig, res, "step: %d, %02x", i, res) + + require.NoErrorf(t, VerifyRFC6979(pub, sig, body), "step: %d", i) + offset += RFC6979SignatureSize + } + + { // Generated by Python + sig := data[offset : offset+RFC6979SignatureSize] + + // It's not equals in Python and Go: + // require.Equal(t, sig, res, "step: %d, %02x", i, res) + + require.NoErrorf(t, VerifyRFC6979(pub, sig, body), "step: %d", i) + + offset += RFC6979SignatureSize + } + } +} diff --git a/test/keys.go b/test/keys.go new file mode 100644 index 0000000..23cf982 --- /dev/null +++ b/test/keys.go @@ -0,0 +1,136 @@ +package test + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "encoding/hex" + "strconv" +) + +// Keys is pre-generated PrivateKeys (SK) for tests only +var Keys = []string{ + "307702010104203ee1fd84dd7199925f8d32f897aaa7f2d6484aa3738e5e0abd03f8240d7c6d8ca00a06082a8648ce3d030107a1440342000475099c302b77664a2508bec1cae47903857b762c62713f190e8d99912ef76737f36191e4c0ea50e47b0e0edbae24fd6529df84f9bd63f87219df3a086efe9195", + "3077020101042035f2b425109b17b1d8f3b5c50daea1091e27d2452bce1126080bd4b98de9bb67a00a06082a8648ce3d030107a144034200045188d33a3113ac77fea0c17137e434d704283c234400b9b70bcdf4829094374abb5818767e460a94f36046ffcef44576fa59ef0e5f31fb86351c06c3d84e156c", + "30770201010420f20cd67ed4ea58307945f5e89a5e016b463fbcad610ee9a7b5e0094a780c63afa00a06082a8648ce3d030107a14403420004c4c574d1bbe7efb2feaeed99e6c03924d6d3c9ad76530437d75c07bff3ddcc0f3f7ef209b4c5156b7395dfa4479dd6aca00d8b0419c2d0ff34de73fad4515694", + "30770201010420335cd4300acc9594cc9a0b8c5b3b3148b29061d019daac1b97d0fbc884f0281ea00a06082a8648ce3d030107a14403420004563eece0b9035e679d28e2d548072773c43ce44a53cb7f30d3597052210dbb70674d8eefac71ca17b3dc6499c9167e833b2c079b2abfe87a5564c2014c6132ca", + "30770201010420063a502c7127688e152ce705f626ca75bf0b62c5106018460f1b2a0d86567546a00a06082a8648ce3d030107a14403420004f8152966ad33b3c2622bdd032f5989fbd63a9a3af34e12eefee912c37defc8801ef16cc2c16120b3359b7426a7609af8f4185a05dcd42e115ae0df0758bc4b4c", + "30770201010420714c3ae55534a1d065ea1213f40a3b276ec50c75eb37ee5934780e1a48027fa2a00a06082a8648ce3d030107a1440342000452d9fd2376f6b3bcb4706cad54ec031d95a1a70414129286c247cd2bc521f73fa8874a6a6466b9d111631645d891e3692688d19c052c244e592a742173ea8984", + "30770201010420324b97d5f2c68e402b6723c600c3a7350559cc90018f9bfce0deed3d57890916a00a06082a8648ce3d030107a1440342000451ec65b2496b1d8ece3efe68a8b57ce7bc75b4171f07fa5b26c63a27fb4f92169c1b15150a8bace13f322b554127eca12155130c0b729872935fd714df05df5e", + "3077020101042086ebcc716545e69a52a7f9a41404583e17984a20d96fafe9a98de0ac420a2f88a00a06082a8648ce3d030107a144034200045f7d63e18e6b896730f45989b7a8d00c0b86c75c2b834d903bc681833592bdcc25cf189e6ddef7b22217fd442b9825f17a985e7e2020b20188486dd53be9073e", + "3077020101042021a5b7932133e23d4ebb7a39713defd99fc94edfc909cf24722754c9077f0d61a00a06082a8648ce3d030107a14403420004d351a4c87ec3b33e62610cb3fd197962c0081bbe1b1b888bc41844f4c6df9cd3fd4637a6f35aa3d4531fecc156b1707504f37f9ef154beebc622afc29ab3f896", + "3077020101042081ef410f78e459fa110908048fc8923fe1e84d7ce75f78f32b8c114c572bfb87a00a06082a8648ce3d030107a144034200046e3859e6ab43c0f45b7891761f0da86a7b62f931f3d963efd3103924920a73b32ce5bc8f14d8fb31e63ccd336b0016eeb951323c915339ca6c4c1ebc01bbeb2b", + "307702010104209dd827fa67faf3912e981b8dbccafb6ded908957ba67cf4c5f37c07d33abb6c5a00a06082a8648ce3d030107a14403420004e5cb5ae6a1bd3861a6b233c9e13fa0183319f601d0f4e99b27461e28f473e822de395d15c1e14d29a6bd4b597547e8c5d09a7dd3a722a739bb76936c1ad43c0e", + "3077020101042005a03e332e1aff5273c52c38ec6c5a1593170ddf8d13989a8a160d894566fc6ba00a06082a8648ce3d030107a144034200045a11611542f07f2d5666de502994ef61f069674513811df42290254c26f71134100fed43ea8ecd9833be9abb42d95be8661f790c15b41ca20db5b4df4f664fb4", + "307702010104206e833f66daf44696cafc63297ff88e16ba13feefa5b6ab3b92a771ff593e96d0a00a06082a8648ce3d030107a14403420004434e0e3ec85c1edaf614f91b7e3203ab4d8e7e1c8a2042223f882fc04da7b1f77f8f2ee3b290ecfa6470a1c416a22b368d05578beb25ec31bcf60aff2e3ffcd4", + "30770201010420937c4796b9fc62fde4521c18289f0e610cf9b5ebf976be8d292bc8306cee2011a00a06082a8648ce3d030107a14403420004ba5951adddf8eb9bc5dac2c03a33584d321f902353c0aadccd3158256b294f5aa9cd5215201d74de2906630d8cefb4f298ff89caa29b5c90f9d15294f8d785bc", + "307702010104204b002204533f9b2fb035087df7f4288e496fc84e09299765de7a6cd61e6a32bca00a06082a8648ce3d030107a1440342000441abcf37a4d0962156c549de8497120b87e5e370a967188ab1d2d7abce53711dfd692a37f30018e2d14030185b16a8e0b9ca61dca82bfe6d8fc55c836355b770", + "3077020101042093ffa35f1977b170a0343986537de367f59ea5a8bd4a8fdd01c5d9700a7282dba00a06082a8648ce3d030107a144034200040e01090b297cf536740b5c0abb15afba03139b0d4b647fdd0c01d457936499c19283cf7b1aee2899923e879c97ddeffe4a1fa2bffc59d331b55982972524b45b", + "307702010104201c1a2209a2b6f445fb63b9c6469d3edc01c99bab10957f0cbe5fad2b1c548975a00a06082a8648ce3d030107a144034200040c8fd2da7bad95b6b3782c0a742476ffcb35e5bc539ea19bbccb5ed05265da3ab51ec39afd01fbee800e05ec0eb94b68854cd9c3de6ab028d011c53085ffc1b3", + "30770201010420b524d8cba99619f1f9559e2fe38b2c6d84a484d38574a92e56977f79eac8b537a00a06082a8648ce3d030107a14403420004a6d7d0db0cc0a46860fb912a7ace42c801d8d693e2678f07c3f5b9ea3cb0311169cbd96b0b9fc78f81e73d2d432b2c224d8d84380125ecc126481ee322335740", + "307702010104207681725fec424a0c75985acfb7be7baed18b43ec7a18c0b47aa757849444557ca00a06082a8648ce3d030107a14403420004bd4453efc74d7dedf442b6fc249848c461a0c636bb6a85c86a194add1f8a5fac9bf0c04ece3f233c5aba2dee0d8a2a11b6a297edae60c0bc0536454ce0b5f9dd", + "30770201010420ae43929b14666baa934684c20a03358cda860b89208824fac56b48f80920edc4a00a06082a8648ce3d030107a14403420004d706b0d86743d6052375aa5aa1a3613c87dccfe704dc85b4ed4f49a84a248a94582202927ec0c082234919f3ce6617152ba0d02497b81c61284261ce86cef905", + "3077020101042089d600f43c47ab98e00225e9b2d4a6c7ab771490f856d4679d9e1e0cca3009d0a00a06082a8648ce3d030107a144034200048515055045543e429173fc8f9f56a070bd4314b2b3005437d8504e6b6885f85101409b933e27c0de11415aee516d0d1b474088a437ece496ceb4f1c131e9ea40", + "3077020101042015518dcf888c7b241dac1c8bfa19d99f7fdba7ba37ed57d69bbbd95bb376ea4ca00a06082a8648ce3d030107a1440342000459e88d92efaa5277d60948feaa0bcd14388da00e35f9bae8282985441788f8beb2b84b71b1ae8aa24d64bb83759b80e3f05c07a791ffe10079c0e1694d74618c", + "307702010104203e840868a96e59ca10f048202cce02e51655a932ff0ac98a7b5589a8df17f580a00a06082a8648ce3d030107a14403420004f296414e914dcefd29bc8a493f8aedc683e5514a8ec5160637bee40ebaa85a421a363c8f7ce3ed113e97d2c4b6d9cd31d21698a54fce8d8e280a6be9ee4fbca9", + "30770201010420aa746067891cf005286d56d53092f77961f828bf5bf11aade18c8a458090d39aa00a06082a8648ce3d030107a144034200044af5ad2dacbb32ab795ab734d26bae6c098bd2ba9ca607542174d61b49ca3c07786aeb0c96908793a63d4f20cd370a77b7ec65e6b285c6337764e7ae3cd5fa1c", + "307702010104207135cbd831d52e778622c21ed035df9e3c6e4128de38fbf4d165a0583b5b4a29a00a06082a8648ce3d030107a1440342000412e2b9e11f288d8db60fbb00456f5969e2816a214a295d8e4d38fbacab6b0a7e0cdb8557e53d408244083f192d8a604d5b764ab44b467e34664ca82e012b60ab", + "3077020101042064b839ca26c42e2e97e94da5589db2de18597a12d6167fdfe0d20e932de747a2a00a06082a8648ce3d030107a1440342000481e90c2173b720447ae28361149598a7245ed51c3881a89353da25b8e574b8c9b2d80b2563efe5d9a0184b57af2431116c8a4ad8071ef2764ca3d3744c638401", + "30770201010420a56df8e6349520d27c36eb1e9675720c702d562842c859cd54b3d866f2cada30a00a06082a8648ce3d030107a14403420004dc08beb5b857f6da13ae1116e40a6e4e4b5aaebc8040eae0b3037c243b1c24def39de670380472df7aa98cb9e0f1132bc4afc0629d80a24c54b8ad600cb24cd4", + "30770201010420bd2dd18485a9667673b2c38c2ad51cc756a199d18fe1100acf29b647a549171ea00a06082a8648ce3d030107a1440342000422825ffe8b3416b6755a7076a7dc6f746ff29ee0a4455dceb0f3262127d51c9bb53f2c204636da8d7a09961274d7c7ba2ef3c771e83fb996ffe3f9882c530ffd", + "307702010104203058a0c8de5c6d4a5c7f64883e7d3c9f5097c8bc073cc482421e903b37123c06a00a06082a8648ce3d030107a14403420004f959705673c2f4112673e43d1d876ca71c64153abb6c9f58d1c3b3c1f8c213ee346833fb695eb533664d596a68e42150a21b405e3a08ed70af5f568275a7a79f", + "307702010104202bd9035bf38e7c4580abc377a6e9c31aa9bdaff90af2ce688eda9a532c83875ea00a06082a8648ce3d030107a14403420004918010ea3387786c6a257996ec74d7ee4e1703b3b811118f4e89fabfef7c694495191848a0d590313a0be9784644ef98e0f0f7e50fed5bee3fa48d66edbcd2b5", + "30770201010420aa055d6cbe96e1cfbe39530bc4b7a976baff53ce399956f0d8241750d3379990a00a06082a8648ce3d030107a1440342000444e8b6deda76c12320a8c5b7a48141ebf5dc9288df79a0f418ab92d82061d10118b8bce9fb200e5009a19fb0e19036762b3ef85440405f43225d6ee3350bf96c", + "30770201010420b8712525a79c7bd3df2a9dbabde1a111078a7ef30687a2efe0f0c4b4a23f2aa0a00a06082a8648ce3d030107a144034200049dc9e3d836a834f6d14ae99dfc70ad9b65c84f351c8dbc4f9b1b61c238051fb1db23e43d4b6e17803e21ebc44fe2f66742e306daa8c4ca7d79c6dd01fc1a4e4e", + "3077020101042086c18b56c4a2264b37c18a7937f026ab07ca6076eeea1ab90376492efb7875d9a00a06082a8648ce3d030107a144034200042f169311f2fae406de3c4a64fec94a22c35972281922a69e7657185997ae59fb3f69ac94295e58681cfbd263f8e6fbce144cc7925b71d90f57de3f3e10588321", + "30770201010420f58221355e1b2da73d66de482ec1edcb8597f3967d00d1356f4678fea6ad67e6a00a06082a8648ce3d030107a14403420004238cc44f02fa566e249a9697a078b9d38eba06012d54a29a430843a18df7a0a4207d704a360399db95eca591f2f81b6c50390467f293a1623b4757bdb4138101", + "30770201010420b10888a0157d524667fd575683bdcded4628a65149fde59b7340781b0cf2e36ea00a06082a8648ce3d030107a14403420004222ba11430b8719929c726aec74e8e70893e2960bc2bbee70fbaa6d88fa2a346adf0c450ea9823f0ba77d334fcd476ea036a62199338d7aa32e56c708d7a8caa", + "30770201010420edf001bd24c92e4f65789aae228223e77df71ce9bbfd7ce4d236ea3648e1f7fea00a06082a8648ce3d030107a1440342000472693c95786ab9f4e7c923338ce98bd068e28b71f84b77e7adb378c2ce2d8f1a2e13833df1afe4569367d7a4eee3abf50124299a28045a0073ea324f5ddb45ea", + "30770201010420e2649e591fc9072dd55573e41fc4ebfdf1db118951e4b7b2a98027ac9a4f7702a00a06082a8648ce3d030107a144034200046e34c9dea1836671f1ef259d7c3ee678c2f92d092af2518413fe9ba153a07ca8e9938784876e90cfa2989a00a83b1ac599c87a8d15be8001e46dfbfe018156a2", + "3077020101042069cd9b710f25613794751aed951004c888d4611aefa45abc23abff218e608290a00a06082a8648ce3d030107a14403420004dcf8ff34ab841720ff8dc08b60a14f41689e65f979a1af69b5e106f4262a2cb0947c9619e980caf20b3e7c8f15e60fc31c5b611c8a58370ba8201c9b6b932bd4", + "307702010104202898cef1944aaf90fddf433390323a02a79938568cf99f6c25bc9aa9e5cddb0aa00a06082a8648ce3d030107a1440342000491a1c20420f5005f5761419e4dcd0d9da0cf2ea4733f6d98a3d0c124f284cabdc65eafd9d2cad9b1122fca791c8b37997feed130c5725ea797cf07c61fb82734", + "30770201010420e568bd3ffa639aa418e7d5bc9e83f3f56690ebf645015ff7f0e216d76045efd5a00a06082a8648ce3d030107a144034200042424b498297124037db950bf2a1e652ba7f977363f4f69d7308531d27bf392219d93cb78f4379b7ffb16f3e7be311e208af2409bd33000fd25a8707ac6bec76b", + "307702010104205163d5d5eea4db97fccc692871f257842fdaca0eca967d29924242f7a2c56ad7a00a06082a8648ce3d030107a144034200044e2ca8312122039c3374db08851710d3b9a2efcbd8f5df004ec7b60a348aee32466f799b5957d39845f451071bb1f3bb99f25bf43196e7c772f7b84f39221b3a", + "30770201010420301eb936d2737886ab2fbf670952f9ba0d324827b81801810bfd60c89e8ca862a00a06082a8648ce3d030107a14403420004455454b1f3828a2328a8925c4c98bd6e37dece276efb3299d8b7d78c9d7e6f978b14d021c07bae0c18a623fc52ab2fec1523a89b2fd0cda373e9c9442a3545f2", + "3077020101042032c12a9bca8070c131b0a46944c17adf35eb44079f3c887fc3b93740bb9c03fca00a06082a8648ce3d030107a14403420004e61da413c4d5dbc6c004089d96a3cb55f4b20b70c544f3823a7a6322c53e134fcb8a885729ef284d68d23e0a58009d48b369f9c4f5a665a8880a48606491dd8a", + "30770201010420aa2b40742722b81c6ffd5c47b94b8be747da259e172a82d27ebc525c8f46d17aa00a06082a8648ce3d030107a14403420004f87a863ed11592cf4f96e837038b105d155f5e09a31386ab4604234e8a975d49a9612b4597b7fb206087b70a26bce4aca31edb253530e6da83ce16beefa99f60", + "307702010104202a70a0c827b4ce8d433e800ab0818b1401b220fadea75feff655251ee4317556a00a06082a8648ce3d030107a14403420004a5c9209fd53dc1ce2c873782ec507db5e0f9cc78292a84ecafc5bab16c2e4d786a882ad77ad999f3d6ba676ad80354ad376dabc4fa03a6c15ead3aa16f213bc5", + "307702010104202787d04901f48c81774171ef2e2a4d440b81f7fa1f12ab93d8e79ffab3416a1ca00a06082a8648ce3d030107a14403420004010d32df4d50343609932a923f11422e3bea5fa1319fb8ce0cc800f66aa38b3f7fda1bc17c824278734baa3d9b7f52262eeacbca21304b74ba4795b5055b1e9f", + "3077020101042032423728a897144d4fb95090ca0ac67a23eb22e2f7f925cbddaf542eeaec8faaa00a06082a8648ce3d030107a14403420004c37f9fec5b1be5b0286300ace6a5d25df8189d29604145a77b6578a4e3956ed3d9af48f8ee1e39868bba9e359e5444984f0428755e29d2012f235c9a56749148", + "30770201010420d5bd2a3867937e0b903d19113e859ca9f6497f4af082894a6911cef3a3a12d35a00a06082a8648ce3d030107a14403420004435b2e891c46023f422119f18a04c75b9322ea4aaddd10a0568438310896388bf7037e98bd5979a6f0839acb07dead1f2f973640dcc11dcee1de8a07c0b3dd80", + "30770201010420590edcf1f2b6ee6c1b836ace33b934597883a00ce84fe812a4b3e22432846972a00a06082a8648ce3d030107a14403420004183d7cad633cb0f4ab774f4dc19b9db87e7ef97b0f4d43ac395d2409dabbe5339dbad661c7c2fd05606e2edb08f8ace660f73bf5232011262d563603f61d2353", + "30770201010420a0ea4e16cf8c7c641d70aea82192fb9303aab6e7b5cd72586ba287d50f4612d6a00a06082a8648ce3d030107a1440342000482a72d31e71f0aea778cb42b324abf853cb4e4e8d4b2ae0e5130480073e911f183134c047a7e1cd41a845a38057ea51a1527923518cbf47c3e195a9f44e1d242", + "307702010104209e04b00c8d0f96ddb2fbb48cfc199905bfbfcc894acb77b56bf16a945a7c7d08a00a06082a8648ce3d030107a1440342000405efd203dcddfb66d514be0de2b35050b83e3738096cd35398165bfdbe34d34c0d96a4e6df503903c75c2c06b66b02b15cd7bf74c147d7a9f0a5e53b83c5762d", + "30770201010420aa69f1cc2cb3482a12af4b1614d6dde01216f1cad1c9f03c681daa8648b75b37a00a06082a8648ce3d030107a1440342000474ffec1297420d0cf730b42942058699d803ab618e1e40ccf9cc17f71f62b3123d863fbf8fae37b6c958892af6151159f74e2a568917bfc2f4e00c55c32b52e7", + "3077020101042090a04300e8d6ed9f44422a2cf93817604bf1f6233c4333ba0db20ab726852fa4a00a06082a8648ce3d030107a144034200049e6f2001baf2b6fb25e3273907ed7320f494de6b5882c4c4b9bcee7ddc60274e064cc68c64325c001f07a505722062d1ca9774a2cc1e0cd28fe5f807865bfcc1", + "3077020101042088945c19c6ce3e63f8d8a421616391d83bec79a0c590f1607b247ffa0c677dd3a00a06082a8648ce3d030107a1440342000492d17d410f9eabf7ae4509a92494e9fe94a72947f24e60c5bb6e12b2cde3c1bfe5305a0d759138069d44268f174136971ecb752df602c282e48d40f43a8734e3", + "3077020101042079d14eacdc4f21dc5284bd8487dcb2c22e9e53e71909474f922bf695f49cf23ea00a06082a8648ce3d030107a1440342000428039292c5bcf3593639bf5835ec9411ffd3ac236c0186697623930b5ca63f32ff41df5217e7def770d9a0de87f61526497bd9aaa95d924e0a17d85958e7c095", + "30770201010420a6ac867ff8d00aaad23198415868a64e59217b4d22474752a146fcb52204dfa5a00a06082a8648ce3d030107a14403420004a5f37a779265c55cd4f5a7f3bffc4679395898046eb9d67d8670be39001de5a7bc010b0d218561626272989c5952e8e0d95d2590f78eec44dc62a46184956301", + "30770201010420df446014577f6081113cd7d33c6ba91b9ac3d083e76f8873358f83129e2d0111a00a06082a8648ce3d030107a14403420004da0c932759f50ad705507f876138c2c6e012764abc8764a6dd609e6ad06099952b120be71690bc091591f1aa8d7d6e9365deddbc958bc87ff150358ad33f7537", + "30770201010420b3351033eaaee3a9ea27cd7dc54aa2c8d787b14b7d428165f1a04a59c6d5b0f2a00a06082a8648ce3d030107a14403420004da3984fb8152403a9fb9068b16f9afb5c900f24230e205567b4405ee3cad2db3ff46968489d494b38d0c85fcc4aecccb61fc00dca54c8fd99ee5bf5e2616f1b7", + "30770201010420deedbcef7f6821f6aab2b15ce198f5eb2064f6eb461a6b7776b4da35c81b1506a00a06082a8648ce3d030107a1440342000405422b86ce66b18e68f0fb14f28e4ed9b1f7ee84f57957f4e4b4c6b0c392e6357e4698fb707f590be1b915622ec8da476071a56919211f6e5e888284d4e33f06", + "3077020101042078c3db0d3b1114cb99f1d0bea0d3aec9067b26964e2b85fe9df4789b24cb3da5a00a06082a8648ce3d030107a144034200046874e52d7d58b6697b407b0c0eea3cfeb528e34fca1589c5031e11aae1ad1f9280e7a4c37ddf28479cd07b4246ce9398e0e24f99946f87e08532fa26b8fb8016", + "30770201010420f0ba42553b146cf088d3a5a3645782fe675d23561897ced7f1270a8d05cfdaaaa00a06082a8648ce3d030107a14403420004c250e12f3aa1fb6261c57cdb091cd90d82917e103711425888477b9da4359d2803aaf0015638294c7c0baa4ec77ba8fceff5ee7f15ea087a4174f58d518006dd", + "307702010104207f2c0fc4b0e418b2d4c72a63fdc27f158f6ad44c26d161f489714525b6a13db1a00a06082a8648ce3d030107a144034200041d83885672021e783d8bd995d187f407bbda2c6bed5e8fabc7c6c5cb304a85eaffa12dad7ba874ac45f4258fffe07534843ff7fe76075470f2c77104d781688f", + "30770201010420d3de828ac9742704d4e6981ce1fc8c473e508eda3a121cda420dacbdf39d48e9a00a06082a8648ce3d030107a14403420004c78abfc4a5c0eb3ee0c9817d1790b7ca9fd528d0bc727f9daf63f4212097538b6888b9de2ae4dff29895500be456fe0ccbee340aecb546d1558b08c3718aaa4a", + "30770201010420d9c4e477b56f2ff0b211acd82b450336276534b350747315152a4923e6e65294a00a06082a8648ce3d030107a14403420004fbd540966b03fe2c2314f20248d345e3e9b92d6a7cfea22d1b5367f01b32d616f317e00cea1f659437b4302610abba8abb0f2bfce0a91b952e9565159c1e464e", + "30770201010420fb84f4a426fa12920c2cf7c2d821280530c0fa93960ded8c20120511dc1d5069a00a06082a8648ce3d030107a14403420004c0177f13c6e00bb9029df089006a332192bdf12a782c60a8d00d110c53db67c344584f22677695a7f1629db1600b0559ced49ac931b08cc6a58e5ea436bde2f8", + "30770201010420653ce060214028f7aa584910f0925d702bde18d52d8e530f07dd5004076eb614a00a06082a8648ce3d030107a1440342000433668d0c9085feae4b285fe260a316e24f24c0bb8e442583e23284bf5a962cd0357cd63ac4d1cdda58afb201bceee911ebe7cf134652dc4390f4e328f6cb5d65", + "307702010104206123b7d5b8c53b2a2a95dd2e42fe550617b7520fe9bd94a99045addb828ad847a00a06082a8648ce3d030107a1440342000487c10fdeaabf8072dcea0dc5b18be4d72f2b8298bc891ea0a11d202438b7598ac588f16a9cd697f8220434d4e15ff4c82daaae63955525633335843069434aea", + "3077020101042000b793c9b8553ee7bec21cd966f5aaff59a07d1fa3fa86e0164bcd2f7f4dd586a00a06082a8648ce3d030107a1440342000419d4179dbeae7fa87e356f0406c327239d34e540cd7db5174a81bd6197738bc72e46fe4bd1512dc4b35950b2c1e78e6f8f54980193be78d45e4d97a837455777", + "307702010104200fb1a771004f6be6300eccd603b9c9e269fbdd69e5eb183d7acad51b0b205b88a00a06082a8648ce3d030107a14403420004d3b7fa62bacff49714ef28a955cdc30f4aef323293ac3aebab824892dfa3306f2ec319f5bca1771b956b4a9b1c2f565dc08b29c07ec84623932a5d6fb59be6c7", + "30770201010420fe6907b91407619fdc95153cd59df061e88095678801008d3901f29c7c434243a00a06082a8648ce3d030107a14403420004796fcea7889128f8060b04e9000381fd3d80fe68f000063b182fe9d8984e740c387c4ed4c6729e8c715c576fe355a9b7dda6890c55b15ae6013fd51e8858b2f2", + "30770201010420111eaff6db3b279d014b45b3da091909f054f37c350c237fe9d51b4342811299a00a06082a8648ce3d030107a144034200047d51f9178725c4134579ac6d0cb84745e0d2068ccf72d30c02dd431547f868d1cb93b5774c7e1eb9582e2151521ff16cdf80b3ba4646d64f7982066f9eb679f0", + "30770201010420631d01e6aaa68e6c36e3425b984df02bc5b54e81951479f7cea8fd1b804bab57a00a06082a8648ce3d030107a14403420004fa1b1ed9ff904f1f050577e05b5175e897d462598fdd323c8ef25f6072dfa43034baa0119e64092fb44f7a04d59d16ba8645f52cfb7775a6536c00f7fc2ee2f1", + "307702010104201ec553d14d45acdf147dba5fcbc3a42a1f763411d5c206d03600ed810b0cf106a00a06082a8648ce3d030107a14403420004e9a309a24d1061204087de10e5bc64b6d45369399a5a402d630ca2d04b34ae9d27d491e5fadd5d082e14454e6b2a572a24904ba2a8dc7430b20d361134188589", + "307702010104206d31e401bb20968106a058f8df70cd5fb8e9aaca0b01a176649712aa594ff600a00a06082a8648ce3d030107a144034200048555a2f9e7256c57b406c729d2d8da12c009f219e81cecb522cb3c494dcc1c76ac6d2f641dafe816065482fb88916e1a719672c82406556e16c32cf90752a92f", + "307702010104208ada3d6ea6000cecbfcc3eafc5d1b0674fabece2b4ed8e9192200021b8861da0a00a06082a8648ce3d030107a14403420004a99e7ed75a2e28e30d8bad1a779f2a48bded02db32b22715c804d8eeadfbf453d063f099874cb170a10d613f6b6b3be0dbdb44c79fc34f81f68aeff570193e78", + "30770201010420d066dfb8f6ba957e19656d5b2362df0fb27075836ec7141ce344f76aa364c3cea00a06082a8648ce3d030107a14403420004597fd2183c21f6d04fa686e813cf7f838594e2e9c95b86ce34b8871674d78cc685b0918fd623e3019d8c7b67104395b1f94fc3338d0772e306572236bab59c39", + "307702010104202c291b04d43060f4c2fd896b7a9b6b4f847fb590f6774b78a0dff2513b32f55ca00a06082a8648ce3d030107a14403420004e80bd7e6445ee6947616e235f59bbecbaa0a49737be3b969363ee8d3cfccbbc42a0a1282de0f27c135c34afad7e5c563c674e3d18f8abcad4a73c8c79dad3efa", + "3077020101042029af306b5c8e677768355076ba86113411023024189e687d8b9c4dee12f156fda00a06082a8648ce3d030107a144034200049d7d21e6e1e586b5868853a3751618de597241215fb2328331d2f273299a11295fe6ccd5d990bf33cf0cdcda9944bf34094d5ffa4e5512ee4a55c9f5a8c25294", + "3077020101042022e65c9fc484173b9c931261d54d2cf34b70deccb19ce0a84ce3b08bc2e0648ba00a06082a8648ce3d030107a14403420004ea9ee4ab7475ebaff6ea2a290fc77aafa4b893447d1a033f40400b4d62ee923a31d06fe5f28dbc2ebec467ebd2e002a9ea72057f0b0c60fe564584a6539376ad", + "307702010104205000583dc21cb6fd26df1c7d6e4efb9b47ceff73c0d94ed453bae0c13a9e5795a00a06082a8648ce3d030107a144034200045a6a5b5886b01f54dfa0788f15d3542aec160843a57e723008d1b984dd572ecb8935662daaba53d756d45442efbae067f52b0b151899a645afb663205babddd3", + "30770201010420997431e73eae00f476bb1a221b4cc9dfd18d787be207b7069141627f61ba752da00a06082a8648ce3d030107a144034200047c89dc8c46a27e20c37b0ecf1150e8b92c2dd4dc534a25545f87a5f0c44fdbf4dee2af5bcdc4012f0acee168aeb55bb4d24738fac105fc056928ff5870491047", + "307702010104207dc10db95a597a80e916d7f8e4e419b609d767538fe9732bcc5f9d783c605a2ba00a06082a8648ce3d030107a144034200042e2ae4fae087a11fcdf9565670164c229337ed87b5056687c6bceeb84108db9a88b9e5d96a0cf121255ceefce0bb5239608768bb841e6687dbd9626222eb5187", + "307702010104209056e22b347f5f1839f1a53f1250d098616ff04db0b49b1fddb18b987930cec7a00a06082a8648ce3d030107a1440342000427cc4c7fb5d7ac047161aee78e812ad264ba25dd878684637308674ea693817b20a5e3672de6a92dfbf82f641268052fa742e6f35ff91c617334f09f89bd1218", + "30770201010420554ea6cfeb2cc4f1e29c08e65317d72731ee03940af9ff6a141b761d5d054db6a00a06082a8648ce3d030107a14403420004a6121746c0553ede0944da8a7f304831fcefb51b40acf78016d41cc45cc5f7e9a1b22bbea028daab5cb4c39cadf84da442749cbfc04536d6f85c3254ec7a0805", + "30770201010420f53ff1c7db3c4e7c734bf7396a1a5364ac2dfe4b794b118aada6bab72cde8969a00a06082a8648ce3d030107a1440342000414b11ec158e3f9d558bd1da1ed0e38c92b1ad55834f3ce08e456747279dd9ed1143cff4f5e8d70189f4b114e3cd609105d6eb8f431f392487e4c9e16a152dba1", + "30770201010420b3f394090547f5dcb2e77cef65e03a3b7d1c953cd0e069553da2795ab0adc950a00a06082a8648ce3d030107a14403420004a1a9dbe5d6dfa2dfb039aebabe96b12faf97c994e1430323d074ecbd90ef075e0fe9dc7d5eef2483d485ffb0b4a01b01e131754fb38059a1365d342d5175397a", + "30770201010420bf13c42fa84c409161f9d73ce20fd85b20c5381914aa2a2375452b34cd352022a00a06082a8648ce3d030107a14403420004e0134214a5349a235cee406ad942ca105ef871a7e4c922ef4769466d8495c78b82f6c49270c8cd913e0cf407cdab679dd9914090ea91122ca9fb654ebcfce57d", + "30770201010420440d975b65bf585d0813137fe041461de59221856eaf255479b5e69721cfb30da00a06082a8648ce3d030107a14403420004935a9626ddb7bd6fbcd2ad9d9333851bbc64b9997cb8e43b1a17f8e9968ed6b0e5d2edf105fbabc9bd745fa2120ac527bbfefb6e8ed96844f80b8e27b6d9a549", + "307702010104209ea2dc59260408165d6c42205aa52e275f81c39d9bf5b1b9c8187ade875e8068a00a06082a8648ce3d030107a14403420004bc570aa24df0306cb761ee9fb22e61f59ae4f11e8804491d8651084f191c800d1e6b16e4bc3693b88f9bef82849f3cd6914a15cae60322c1f4822a2bdf426782", + "30770201010420505b596fb71a2e36c0ba07da03442a721f3f1832dcac19631d6c11b36ab81986a00a06082a8648ce3d030107a1440342000472cfb26cf07faa4e6e9d328214677b5eb51cd2e35717ac661d732115e592a07482bf966a31792cc993bdf816a732069ed423871b53fb3c7eabab2f4d3d272013", + "3077020101042089a9d5b397c521db4bb4a5f3e8f2043e43bb5617a2070e7bfa30dd2dbf1815a1a00a06082a8648ce3d030107a1440342000468d2aeaf641b839095644cfd4b72ab97d0bf3fae1ed36e9f81d9aff333b0123f7b846f6ca61dbbd4e10988e740463addef793994a1498987883ecf237f18bc40", + "307702010104200919a89aedb4e20cfcd2cb568c8de18b1b60b5da17aaea3be9804eb5bc3280f5a00a06082a8648ce3d030107a14403420004139812ec6bd62fd3ce71040d87cc07671948ff82300fae5f3af80dcd4e22c870c0102c4add460b2cbbeeb298f58037fc645da20aa8f5531a5ff56d3e5b2d1944", + "30770201010420b145fc69cfabff378f390f0a99fb98ddc8ba9228cb1adf9c7099c6393a24567aa00a06082a8648ce3d030107a14403420004b660084cb05e005fb163011663fee6946f354714565069968f16e89e9a7aac45610f05502ff9d9e3cd0fdc88083bd8840a518b71135e59a0f0f235636d5eb7c4", + "3077020101042082d39168f289e784ace49bfdd523297b524c494f83fe7d04dd2f055b48d636b9a00a06082a8648ce3d030107a14403420004ea4021da5eec4e7f333059625ecbad3969676cf625cbf0da316f55f50ccd40e6174fdb7023c07abdb3ca91203acbcb5e78e1601f1a9aa616c5019ac5b2222ff4", + "3077020101042066a1ebc23e993674bfdc3b9721c280b7f3c1599903063ea7899b848b942a6169a00a06082a8648ce3d030107a144034200046bdb182c6c0c1f9ea898c3847bc4b46014cb8da6a02d75b7bed3c4a9a4e9c8836d4ce22fe68b68ae56a91fb435c7ea8f05bca8e8fcb1d6b77770d419f99e51da", + "30770201010420fa2cda21b761c46fcc5b54d47b045e24affdb95425e859bb367a07950119ab6ba00a06082a8648ce3d030107a144034200044b9e4cee102ad23fea3357f8f5f95ab9d60d34086ba4b39d5f37cbc61998ac9658ec56033ad72977d41e449d449f5aac2bc653ea8038fc04a011ff02ec49e088", + "3077020101042028acfb3c41b7be1d9d0506ac3702c363ffd767dd738dc8ab581ad7add2ec8872a00a06082a8648ce3d030107a144034200047467dedfb8c9a7d9496d4898d6ace0fba063545ab0d345d8b63b90871927ed269645a745a7335ca511d86a366f24e7832477842b4041a9ab564c5fbce49e4df8", + "307702010104202e57b8b867bd95a8dfcdd2cb8f82ea41bff21610019afd6e2367e755dec5b944a00a06082a8648ce3d030107a144034200048f97eb2d6ee2d3da8746d8d4f84469ea765fb0d1412b167b6d8a916b5f968b4d64ede5ea6d6e08ec0de192262fcb3ebed49e9d17858261affed84827b38c6cc9", + "3077020101042021a904281e4c31386ce34a5b52af3a068caa65819fbcf0ca76ab6041ecdaf454a00a06082a8648ce3d030107a1440342000405f9b7894a97fcddfc3285b8e974718606616fe07c70b7ab2bfb28a85fb3014c2610ab9e8e6da8ae3da032837d3a14b1e791d2633bdd8551b4817a080b9aa697", + "3077020101042089c2c73d08bd03da4c3111aa0b78bb1edc5243d8e119513035d3741e851dec1ca00a06082a8648ce3d030107a14403420004ec9ebc34f45150334fd1d8c92274fe43c5b3b059f15cb1963f6cf7d54bc6b1b0b4ef1c5d56d2d06ab54ce2e7606e0fa5d2f188a2d593b22d9cf6a0098aa00cb6", +} + +// DecodeKey method for test usage only +func DecodeKey(i int) *ecdsa.PrivateKey { + var ( + err error + key *ecdsa.PrivateKey + ) + + if i < 0 { + if key, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader); err != nil { + panic("could not generate uniq key") + } + } else if current, size := i, len(Keys); current >= size { + panic("add more test keys, used " + strconv.Itoa(current) + " from " + strconv.Itoa(size)) + } else if buf, err := hex.DecodeString(Keys[i]); err != nil { + panic("could not hex.Decode: " + err.Error()) + } else if key, err = x509.ParseECPrivateKey(buf); err != nil { + panic("could x509.ParseECPrivateKey: " + err.Error()) + } + + return key +} diff --git a/wif.go b/wif.go new file mode 100644 index 0000000..3ada5a2 --- /dev/null +++ b/wif.go @@ -0,0 +1,62 @@ +package crypto + +import ( + "bytes" + "crypto/ecdsa" + "crypto/sha256" + + "github.com/mr-tron/base58" + "github.com/nspcc-dev/neofs-crypto/internal" + "github.com/pkg/errors" +) + +const ( + // WIFLength constant length of WIF string. + WIFLength = 38 + + // ErrBadWIF when passed WIF-string could not be decoded from base58. + ErrBadWIF = internal.Error("bad wif") + + // ErrBadChecksum when passed WIF-string could not be verified + // by last 4 bytes signature. + ErrBadChecksum = internal.Error("bad checksum") + + // ErrEmptyPrivateKey when PK passed into WIFEncode method is nil + ErrEmptyPrivateKey = internal.Error("empty private key") +) + +func wifCheckSum(data []byte) []byte { + sum := sha256.Sum256(data) + sum = sha256.Sum256(sum[:]) + + return sum[:4] +} + +// WIFEncode encodes the given private key into a WIF string. +func WIFEncode(key *ecdsa.PrivateKey) (string, error) { + if key == nil || key.D == nil { + return "", ErrEmptyPrivateKey + } + + data := make([]byte, WIFLength) + data[0] = 0x80 + data[33] = 0x01 + copy(data[1:33], key.D.Bytes()) + copy(data[34:], wifCheckSum(data[:34])) + + return base58.Encode(data), nil +} + +// WIFDecode decoded the given WIF string into a private key. +func WIFDecode(wif string) (*ecdsa.PrivateKey, error) { + data, err := base58.Decode(wif) + if err != nil { + return nil, errors.Wrap(ErrBadWIF, err.Error()) + } else if actual := len(data); actual != WIFLength { + return nil, errors.Wrapf(ErrBadWIF, "expect: %d, actual: %d", WIFLength, actual) + } else if sum := wifCheckSum(data[:34]); !bytes.Equal(data[34:], sum) { + return nil, ErrBadChecksum + } + + return UnmarshalPrivateKey(data[1:33]) +} diff --git a/wif_test.go b/wif_test.go new file mode 100644 index 0000000..e8c8240 --- /dev/null +++ b/wif_test.go @@ -0,0 +1,144 @@ +package crypto + +import ( + "crypto/ecdsa" + "encoding/hex" + "testing" + + "github.com/nspcc-dev/neofs-crypto/test" + "github.com/pkg/errors" + "github.com/stretchr/testify/require" +) + +type ( + NPPrompt struct { + Key *ecdsa.PrivateKey + WIF string + } + + WIFTestCase struct { + Error error + Name string + WIF string + Key *ecdsa.PrivateKey + } +) + +func npPromptSKWIF(t *testing.T) *NPPrompt { + key, err := hex.DecodeString("c428b4a06f166fde9f8afcf918194acdde35aa2612ecf42fe0c94273425ded21") + require.NoError(t, err) + + sk, err := UnmarshalPrivateKey(key) + require.NoError(t, err) + + return &NPPrompt{ + Key: sk, + WIF: "L3o221BojgcCPYgdbXsm6jn7ayTZ72xwREvBHXKknR8VJ3G4WmjB", + } +} + +func TestWIF(t *testing.T) { + kw := npPromptSKWIF(t) + + t.Run("encoding", func(t *testing.T) { + cases := []WIFTestCase{ + { + Name: "success #1", + Key: test.DecodeKey(0), + WIF: "KyKwsDQhb6ncTw9wfoJqMXUABTsMLi36u7BZBBKo5uzmGFEHHDVu", + }, + + { + Name: "success #2", + Key: test.DecodeKey(1), + WIF: "Ky2aXYjbxUoMoLaPhyEw8U37BVD6N1rw7mfbbxDv3ULqM9dta7ZW", + }, + + { + Name: "success #3", + Key: test.DecodeKey(2), + WIF: "L5LDzGxboJv2CqWKp7v8UGddAMf834DZvMK7muRgNqXTSTwG8pRJ", + }, + + { + Name: "np-prompt generated SK/WIF", + Key: kw.Key, + WIF: kw.WIF, + }, + + { + Name: "empty key", + Error: ErrEmptyPrivateKey, + Key: nil, + }, + } + + for i := range cases { + current := cases[i] + t.Run(current.Name, func(t *testing.T) { + actual, err := WIFEncode(current.Key) + switch current.Error { + case nil: + require.NoError(t, err) + require.Equal(t, current.WIF, actual) + default: + require.EqualError(t, errors.Cause(err), current.Error.Error()) + } + }) + } + }) + t.Run("decoding", func(t *testing.T) { + cases := []WIFTestCase{ + { + Name: "success #1", + Key: test.DecodeKey(0), + WIF: "KyKwsDQhb6ncTw9wfoJqMXUABTsMLi36u7BZBBKo5uzmGFEHHDVu", + }, + { + Name: "success #2", + Key: test.DecodeKey(1), + WIF: "Ky2aXYjbxUoMoLaPhyEw8U37BVD6N1rw7mfbbxDv3ULqM9dta7ZW", + }, + { + Name: "success #3", + Key: test.DecodeKey(2), + WIF: "L5LDzGxboJv2CqWKp7v8UGddAMf834DZvMK7muRgNqXTSTwG8pRJ", + }, + { + Name: "np-prompt generated SK/WIF", + Key: kw.Key, + WIF: kw.WIF, + }, + { + Error: ErrBadChecksum, + Name: "bad checksum", + WIF: "KyKwsDQhb6ncTw9wfoJqMXUABTsMLi36u7BZBBKo5uzmGF7rYxf5", + }, + { + Error: ErrBadWIF, + Name: "bad wif", + WIF: "bad_wif", + }, + + { + Error: ErrBadWIF, + Name: "bad wif length", + WIF: "L5LDzGxboJv2CqWKp7v8UGddAMf834DZvMK7muRgNqXTSTueCExxDEADBEAF", + }, + } + + for i := range cases { + current := cases[i] + t.Run(current.Name, func(t *testing.T) { + actual, err := WIFDecode(current.WIF) + switch current.Error { + case nil: + require.NoError(t, err) + require.Equal(t, current.Key, actual) + default: + require.EqualError(t, errors.Cause(err), current.Error.Error()) + } + }) + } + }) +}