Merge pull request 'Allow pushmirror to use publickey authentication' (#4819) from ironmagma/forgejo:publickey-auth-push-mirror into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/4819 Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
This commit is contained in:
commit
5dbacb70f4
24 changed files with 648 additions and 66 deletions
|
@ -1,5 +1,6 @@
|
|||
// Copyright 2015 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package git
|
||||
|
@ -18,6 +19,7 @@ import (
|
|||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/proxy"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
|
@ -190,17 +192,39 @@ func CloneWithArgs(ctx context.Context, args TrustedCmdArgs, from, to string, op
|
|||
|
||||
// PushOptions options when push to remote
|
||||
type PushOptions struct {
|
||||
Remote string
|
||||
Branch string
|
||||
Force bool
|
||||
Mirror bool
|
||||
Env []string
|
||||
Timeout time.Duration
|
||||
Remote string
|
||||
Branch string
|
||||
Force bool
|
||||
Mirror bool
|
||||
Env []string
|
||||
Timeout time.Duration
|
||||
PrivateKeyPath string
|
||||
}
|
||||
|
||||
// Push pushs local commits to given remote branch.
|
||||
func Push(ctx context.Context, repoPath string, opts PushOptions) error {
|
||||
cmd := NewCommand(ctx, "push")
|
||||
|
||||
if opts.PrivateKeyPath != "" {
|
||||
// Preserve the behavior that existing environments are used if no
|
||||
// environments are passed.
|
||||
if len(opts.Env) == 0 {
|
||||
opts.Env = os.Environ()
|
||||
}
|
||||
|
||||
// Use environment because it takes precedence over using -c core.sshcommand
|
||||
// and it's possible that a system might have an existing GIT_SSH_COMMAND
|
||||
// environment set.
|
||||
opts.Env = append(opts.Env, "GIT_SSH_COMMAND=ssh"+
|
||||
fmt.Sprintf(` -i %s`, opts.PrivateKeyPath)+
|
||||
" -o IdentitiesOnly=yes"+
|
||||
// This will store new SSH host keys and verify connections to existing
|
||||
// host keys, but it doesn't allow replacement of existing host keys. This
|
||||
// means TOFU is used for Git over SSH pushes.
|
||||
" -o StrictHostKeyChecking=accept-new"+
|
||||
" -o UserKnownHostsFile="+filepath.Join(setting.SSH.RootPath, "known_hosts"))
|
||||
}
|
||||
|
||||
if opts.Force {
|
||||
cmd.AddArguments("-f")
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ package keying
|
|||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
"golang.org/x/crypto/hkdf"
|
||||
|
@ -44,6 +45,9 @@ func Init(ikm []byte) {
|
|||
// This must be a hardcoded string and must not be arbitrarily constructed.
|
||||
type Context string
|
||||
|
||||
// Used for the `push_mirror` table.
|
||||
var ContextPushMirror Context = "pushmirror"
|
||||
|
||||
// Derive *the* key for a given context, this is a determistic function. The
|
||||
// same key will be provided for the same context.
|
||||
func DeriveKey(context Context) *Key {
|
||||
|
@ -109,3 +113,13 @@ func (k *Key) Decrypt(ciphertext, additionalData []byte) ([]byte, error) {
|
|||
|
||||
return e.Open(nil, nonce, ciphertext, additionalData)
|
||||
}
|
||||
|
||||
// ColumnAndID generates a context that can be used as additional context for
|
||||
// encrypting and decrypting data. It requires the column name and the row ID
|
||||
// (this requires to be known beforehand). Be careful when using this, as the
|
||||
// table name isn't part of this context. This means it's not bound to a
|
||||
// particular table. The table should be part of the context that the key was
|
||||
// derived for, in which case it binds through that.
|
||||
func ColumnAndID(column string, id int64) []byte {
|
||||
return binary.BigEndian.AppendUint64(append([]byte(column), ':'), uint64(id))
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
package keying_test
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/keying"
|
||||
|
@ -94,3 +95,17 @@ func TestKeying(t *testing.T) {
|
|||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestKeyingColumnAndID(t *testing.T) {
|
||||
assert.EqualValues(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x3a, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, keying.ColumnAndID("table", math.MinInt64))
|
||||
assert.EqualValues(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x3a, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, keying.ColumnAndID("table", -1))
|
||||
assert.EqualValues(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x3a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, keying.ColumnAndID("table", 0))
|
||||
assert.EqualValues(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x3a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01}, keying.ColumnAndID("table", 1))
|
||||
assert.EqualValues(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x3a, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, keying.ColumnAndID("table", math.MaxInt64))
|
||||
|
||||
assert.EqualValues(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x32, 0x3a, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, keying.ColumnAndID("table2", math.MinInt64))
|
||||
assert.EqualValues(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x32, 0x3a, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, keying.ColumnAndID("table2", -1))
|
||||
assert.EqualValues(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x32, 0x3a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, keying.ColumnAndID("table2", 0))
|
||||
assert.EqualValues(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x32, 0x3a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01}, keying.ColumnAndID("table2", 1))
|
||||
assert.EqualValues(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x32, 0x3a, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, keying.ColumnAndID("table2", math.MaxInt64))
|
||||
}
|
||||
|
|
|
@ -60,6 +60,10 @@ func endpointFromURL(rawurl string) *url.URL {
|
|||
case "git":
|
||||
u.Scheme = "https"
|
||||
return u
|
||||
case "ssh":
|
||||
u.Scheme = "https"
|
||||
u.User = nil
|
||||
return u
|
||||
case "file":
|
||||
return u
|
||||
default:
|
||||
|
|
|
@ -12,6 +12,7 @@ type CreatePushMirrorOption struct {
|
|||
RemotePassword string `json:"remote_password"`
|
||||
Interval string `json:"interval"`
|
||||
SyncOnCommit bool `json:"sync_on_commit"`
|
||||
UseSSH bool `json:"use_ssh"`
|
||||
}
|
||||
|
||||
// PushMirror represents information of a push mirror
|
||||
|
@ -27,4 +28,5 @@ type PushMirror struct {
|
|||
LastError string `json:"last_error"`
|
||||
Interval string `json:"interval"`
|
||||
SyncOnCommit bool `json:"sync_on_commit"`
|
||||
PublicKey string `json:"public_key"`
|
||||
}
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strconv"
|
||||
|
@ -13,6 +16,7 @@ import (
|
|||
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
@ -229,3 +233,23 @@ func ReserveLineBreakForTextarea(input string) string {
|
|||
// Other than this, we should respect the original content, even leading or trailing spaces.
|
||||
return strings.ReplaceAll(input, "\r\n", "\n")
|
||||
}
|
||||
|
||||
// GenerateSSHKeypair generates a ed25519 SSH-compatible keypair.
|
||||
func GenerateSSHKeypair() (publicKey, privateKey []byte, err error) {
|
||||
public, private, err := ed25519.GenerateKey(nil)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("ed25519.GenerateKey: %w", err)
|
||||
}
|
||||
|
||||
privPEM, err := ssh.MarshalPrivateKey(private, "")
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("ssh.MarshalPrivateKey: %w", err)
|
||||
}
|
||||
|
||||
sshPublicKey, err := ssh.NewPublicKey(public)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("ssh.NewPublicKey: %w", err)
|
||||
}
|
||||
|
||||
return ssh.MarshalAuthorizedKey(sshPublicKey), pem.EncodeToMemory(privPEM), nil
|
||||
}
|
||||
|
|
|
@ -1,14 +1,19 @@
|
|||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package util
|
||||
package util_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
@ -43,7 +48,7 @@ func TestURLJoin(t *testing.T) {
|
|||
newTest("/a/b/c#hash",
|
||||
"/a", "b/c#hash"),
|
||||
} {
|
||||
assert.Equal(t, test.Expected, URLJoin(test.Base, test.Elements...))
|
||||
assert.Equal(t, test.Expected, util.URLJoin(test.Base, test.Elements...))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -59,7 +64,7 @@ func TestIsEmptyString(t *testing.T) {
|
|||
}
|
||||
|
||||
for _, v := range cases {
|
||||
assert.Equal(t, v.expected, IsEmptyString(v.s))
|
||||
assert.Equal(t, v.expected, util.IsEmptyString(v.s))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -100,42 +105,42 @@ func Test_NormalizeEOL(t *testing.T) {
|
|||
unix := buildEOLData(data1, "\n")
|
||||
mac := buildEOLData(data1, "\r")
|
||||
|
||||
assert.Equal(t, unix, NormalizeEOL(dos))
|
||||
assert.Equal(t, unix, NormalizeEOL(mac))
|
||||
assert.Equal(t, unix, NormalizeEOL(unix))
|
||||
assert.Equal(t, unix, util.NormalizeEOL(dos))
|
||||
assert.Equal(t, unix, util.NormalizeEOL(mac))
|
||||
assert.Equal(t, unix, util.NormalizeEOL(unix))
|
||||
|
||||
dos = buildEOLData(data2, "\r\n")
|
||||
unix = buildEOLData(data2, "\n")
|
||||
mac = buildEOLData(data2, "\r")
|
||||
|
||||
assert.Equal(t, unix, NormalizeEOL(dos))
|
||||
assert.Equal(t, unix, NormalizeEOL(mac))
|
||||
assert.Equal(t, unix, NormalizeEOL(unix))
|
||||
assert.Equal(t, unix, util.NormalizeEOL(dos))
|
||||
assert.Equal(t, unix, util.NormalizeEOL(mac))
|
||||
assert.Equal(t, unix, util.NormalizeEOL(unix))
|
||||
|
||||
assert.Equal(t, []byte("one liner"), NormalizeEOL([]byte("one liner")))
|
||||
assert.Equal(t, []byte("\n"), NormalizeEOL([]byte("\n")))
|
||||
assert.Equal(t, []byte("\ntwo liner"), NormalizeEOL([]byte("\ntwo liner")))
|
||||
assert.Equal(t, []byte("two liner\n"), NormalizeEOL([]byte("two liner\n")))
|
||||
assert.Equal(t, []byte{}, NormalizeEOL([]byte{}))
|
||||
assert.Equal(t, []byte("one liner"), util.NormalizeEOL([]byte("one liner")))
|
||||
assert.Equal(t, []byte("\n"), util.NormalizeEOL([]byte("\n")))
|
||||
assert.Equal(t, []byte("\ntwo liner"), util.NormalizeEOL([]byte("\ntwo liner")))
|
||||
assert.Equal(t, []byte("two liner\n"), util.NormalizeEOL([]byte("two liner\n")))
|
||||
assert.Equal(t, []byte{}, util.NormalizeEOL([]byte{}))
|
||||
|
||||
assert.Equal(t, []byte("mix\nand\nmatch\n."), NormalizeEOL([]byte("mix\r\nand\rmatch\n.")))
|
||||
assert.Equal(t, []byte("mix\nand\nmatch\n."), util.NormalizeEOL([]byte("mix\r\nand\rmatch\n.")))
|
||||
}
|
||||
|
||||
func Test_RandomInt(t *testing.T) {
|
||||
randInt, err := CryptoRandomInt(255)
|
||||
randInt, err := util.CryptoRandomInt(255)
|
||||
assert.GreaterOrEqual(t, randInt, int64(0))
|
||||
assert.LessOrEqual(t, randInt, int64(255))
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func Test_RandomString(t *testing.T) {
|
||||
str1, err := CryptoRandomString(32)
|
||||
str1, err := util.CryptoRandomString(32)
|
||||
require.NoError(t, err)
|
||||
matches, err := regexp.MatchString(`^[a-zA-Z0-9]{32}$`, str1)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, matches)
|
||||
|
||||
str2, err := CryptoRandomString(32)
|
||||
str2, err := util.CryptoRandomString(32)
|
||||
require.NoError(t, err)
|
||||
matches, err = regexp.MatchString(`^[a-zA-Z0-9]{32}$`, str1)
|
||||
require.NoError(t, err)
|
||||
|
@ -143,13 +148,13 @@ func Test_RandomString(t *testing.T) {
|
|||
|
||||
assert.NotEqual(t, str1, str2)
|
||||
|
||||
str3, err := CryptoRandomString(256)
|
||||
str3, err := util.CryptoRandomString(256)
|
||||
require.NoError(t, err)
|
||||
matches, err = regexp.MatchString(`^[a-zA-Z0-9]{256}$`, str3)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, matches)
|
||||
|
||||
str4, err := CryptoRandomString(256)
|
||||
str4, err := util.CryptoRandomString(256)
|
||||
require.NoError(t, err)
|
||||
matches, err = regexp.MatchString(`^[a-zA-Z0-9]{256}$`, str4)
|
||||
require.NoError(t, err)
|
||||
|
@ -159,34 +164,34 @@ func Test_RandomString(t *testing.T) {
|
|||
}
|
||||
|
||||
func Test_RandomBytes(t *testing.T) {
|
||||
bytes1, err := CryptoRandomBytes(32)
|
||||
bytes1, err := util.CryptoRandomBytes(32)
|
||||
require.NoError(t, err)
|
||||
|
||||
bytes2, err := CryptoRandomBytes(32)
|
||||
bytes2, err := util.CryptoRandomBytes(32)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.NotEqual(t, bytes1, bytes2)
|
||||
|
||||
bytes3, err := CryptoRandomBytes(256)
|
||||
bytes3, err := util.CryptoRandomBytes(256)
|
||||
require.NoError(t, err)
|
||||
|
||||
bytes4, err := CryptoRandomBytes(256)
|
||||
bytes4, err := util.CryptoRandomBytes(256)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.NotEqual(t, bytes3, bytes4)
|
||||
}
|
||||
|
||||
func TestOptionalBoolParse(t *testing.T) {
|
||||
assert.Equal(t, optional.None[bool](), OptionalBoolParse(""))
|
||||
assert.Equal(t, optional.None[bool](), OptionalBoolParse("x"))
|
||||
assert.Equal(t, optional.None[bool](), util.OptionalBoolParse(""))
|
||||
assert.Equal(t, optional.None[bool](), util.OptionalBoolParse("x"))
|
||||
|
||||
assert.Equal(t, optional.Some(false), OptionalBoolParse("0"))
|
||||
assert.Equal(t, optional.Some(false), OptionalBoolParse("f"))
|
||||
assert.Equal(t, optional.Some(false), OptionalBoolParse("False"))
|
||||
assert.Equal(t, optional.Some(false), util.OptionalBoolParse("0"))
|
||||
assert.Equal(t, optional.Some(false), util.OptionalBoolParse("f"))
|
||||
assert.Equal(t, optional.Some(false), util.OptionalBoolParse("False"))
|
||||
|
||||
assert.Equal(t, optional.Some(true), OptionalBoolParse("1"))
|
||||
assert.Equal(t, optional.Some(true), OptionalBoolParse("t"))
|
||||
assert.Equal(t, optional.Some(true), OptionalBoolParse("True"))
|
||||
assert.Equal(t, optional.Some(true), util.OptionalBoolParse("1"))
|
||||
assert.Equal(t, optional.Some(true), util.OptionalBoolParse("t"))
|
||||
assert.Equal(t, optional.Some(true), util.OptionalBoolParse("True"))
|
||||
}
|
||||
|
||||
// Test case for any function which accepts and returns a single string.
|
||||
|
@ -209,7 +214,7 @@ var upperTests = []StringTest{
|
|||
|
||||
func TestToUpperASCII(t *testing.T) {
|
||||
for _, tc := range upperTests {
|
||||
assert.Equal(t, ToUpperASCII(tc.in), tc.out)
|
||||
assert.Equal(t, util.ToUpperASCII(tc.in), tc.out)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -217,27 +222,56 @@ func BenchmarkToUpper(b *testing.B) {
|
|||
for _, tc := range upperTests {
|
||||
b.Run(tc.in, func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
ToUpperASCII(tc.in)
|
||||
util.ToUpperASCII(tc.in)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestToTitleCase(t *testing.T) {
|
||||
assert.Equal(t, `Foo Bar Baz`, ToTitleCase(`foo bar baz`))
|
||||
assert.Equal(t, `Foo Bar Baz`, ToTitleCase(`FOO BAR BAZ`))
|
||||
assert.Equal(t, `Foo Bar Baz`, util.ToTitleCase(`foo bar baz`))
|
||||
assert.Equal(t, `Foo Bar Baz`, util.ToTitleCase(`FOO BAR BAZ`))
|
||||
}
|
||||
|
||||
func TestToPointer(t *testing.T) {
|
||||
assert.Equal(t, "abc", *ToPointer("abc"))
|
||||
assert.Equal(t, 123, *ToPointer(123))
|
||||
assert.Equal(t, "abc", *util.ToPointer("abc"))
|
||||
assert.Equal(t, 123, *util.ToPointer(123))
|
||||
abc := "abc"
|
||||
assert.NotSame(t, &abc, ToPointer(abc))
|
||||
assert.NotSame(t, &abc, util.ToPointer(abc))
|
||||
val123 := 123
|
||||
assert.NotSame(t, &val123, ToPointer(val123))
|
||||
assert.NotSame(t, &val123, util.ToPointer(val123))
|
||||
}
|
||||
|
||||
func TestReserveLineBreakForTextarea(t *testing.T) {
|
||||
assert.Equal(t, "test\ndata", ReserveLineBreakForTextarea("test\r\ndata"))
|
||||
assert.Equal(t, "test\ndata\n", ReserveLineBreakForTextarea("test\r\ndata\r\n"))
|
||||
assert.Equal(t, "test\ndata", util.ReserveLineBreakForTextarea("test\r\ndata"))
|
||||
assert.Equal(t, "test\ndata\n", util.ReserveLineBreakForTextarea("test\r\ndata\r\n"))
|
||||
}
|
||||
|
||||
const (
|
||||
testPublicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAOhB7/zzhC+HXDdGOdLwJln5NYwm6UNXx3chmQSVTG4\n"
|
||||
testPrivateKey = `-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtz
|
||||
c2gtZWQyNTUxOQAAACADoQe/884Qvh1w3RjnS8CZZ+TWMJulDV8d3IZkElUxuAAA
|
||||
AIggISIjICEiIwAAAAtzc2gtZWQyNTUxOQAAACADoQe/884Qvh1w3RjnS8CZZ+TW
|
||||
MJulDV8d3IZkElUxuAAAAEAAAQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0e
|
||||
HwOhB7/zzhC+HXDdGOdLwJln5NYwm6UNXx3chmQSVTG4AAAAAAECAwQF
|
||||
-----END OPENSSH PRIVATE KEY-----` + "\n"
|
||||
)
|
||||
|
||||
func TestGeneratingEd25519Keypair(t *testing.T) {
|
||||
defer test.MockProtect(&rand.Reader)()
|
||||
|
||||
// Only 32 bytes needs to be provided to generate a ed25519 keypair.
|
||||
// And another 32 bytes are required, which is included as random value
|
||||
// in the OpenSSH format.
|
||||
b := make([]byte, 64)
|
||||
for i := 0; i < 64; i++ {
|
||||
b[i] = byte(i)
|
||||
}
|
||||
rand.Reader = bytes.NewReader(b)
|
||||
|
||||
publicKey, privateKey, err := util.GenerateSSHKeypair()
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, testPublicKey, string(publicKey))
|
||||
assert.EqualValues(t, testPrivateKey, string(privateKey))
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue