From ef4e256e220c19f6e94e4ece090d6db253b7b3af Mon Sep 17 00:00:00 2001 From: deatil <2217957370@qq.com> Date: Sun, 12 Jan 2025 18:09:10 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=20OPENSSH?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cryptobin/ssh/check.go | 32 +++++ cryptobin/ssh/create.go | 110 +++++++++++++++ cryptobin/ssh/error.go | 17 +++ cryptobin/ssh/from.go | 221 ++++++++++++++++++++++++++++++ cryptobin/ssh/get.go | 74 ++++++++++ cryptobin/ssh/make.go | 36 +++++ cryptobin/ssh/on.go | 14 ++ cryptobin/ssh/parse.go | 58 ++++++++ cryptobin/ssh/sign.go | 78 +++++++++++ cryptobin/ssh/sign_test.go | 193 ++++++++++++++++++++++++++ cryptobin/ssh/ssh.go | 110 +++++++++++++++ cryptobin/ssh/ssh_test.go | 268 +++++++++++++++++++++++++++++++++++++ cryptobin/ssh/to.go | 53 ++++++++ cryptobin/ssh/with.go | 165 +++++++++++++++++++++++ go.mod | 6 +- ssh/key_dsa.go | 89 ++++++++++++ ssh/key_rsa.go | 3 +- ssh/key_setting.go | 8 ++ ssh/ssh_test.go | 23 ++++ 19 files changed, 1554 insertions(+), 4 deletions(-) create mode 100644 cryptobin/ssh/check.go create mode 100644 cryptobin/ssh/create.go create mode 100644 cryptobin/ssh/error.go create mode 100644 cryptobin/ssh/from.go create mode 100644 cryptobin/ssh/get.go create mode 100644 cryptobin/ssh/make.go create mode 100644 cryptobin/ssh/on.go create mode 100644 cryptobin/ssh/parse.go create mode 100644 cryptobin/ssh/sign.go create mode 100644 cryptobin/ssh/sign_test.go create mode 100644 cryptobin/ssh/ssh.go create mode 100644 cryptobin/ssh/ssh_test.go create mode 100644 cryptobin/ssh/to.go create mode 100644 cryptobin/ssh/with.go create mode 100644 ssh/key_dsa.go diff --git a/cryptobin/ssh/check.go b/cryptobin/ssh/check.go new file mode 100644 index 00000000..eb0bae91 --- /dev/null +++ b/cryptobin/ssh/check.go @@ -0,0 +1,32 @@ +package ecgdsa + +import ( + "crypto" + "reflect" +) + +type publicKeyEqual interface { + Equal(x crypto.PublicKey) bool +} + +// 检测公钥私钥是否匹配 +func (this SSH) CheckKeyPair() bool { + // 私钥导出的公钥 + pubKeyFromPriKey := this.MakePublicKey().publicKey + + if pubKeyFromPriKey == nil || this.publicKey == nil { + return false + } + + if pubkeyEqual, ok := pubKeyFromPriKey.(publicKeyEqual); ok { + if pubkeyEqual.Equal(this.publicKey) { + return true + } + } + + if reflect.DeepEqual(pubKeyFromPriKey, this.publicKey) { + return true + } + + return false +} diff --git a/cryptobin/ssh/create.go b/cryptobin/ssh/create.go new file mode 100644 index 00000000..343e76f9 --- /dev/null +++ b/cryptobin/ssh/create.go @@ -0,0 +1,110 @@ +package ecgdsa + +import ( + "errors" + "crypto/rand" + "encoding/pem" + + "github.com/deatil/go-cryptobin/ssh" +) + +type ( + // 配置 + Opts = ssh.Opts +) + +var ( + // get Cipher + GetCipherFromName = ssh.GetCipherFromName + + // Default options + DefaultOpts = ssh.DefaultOpts +) + +// 生成私钥 pem 数据 +func (this SSH) CreatePrivateKey() SSH { + return this.CreateOpensshPrivateKey() +} + +// 生成私钥带密码 pem 数据, PKCS1 别名 +func (this SSH) CreatePrivateKeyWithPassword(password []byte, opts ...Opts) SSH { + return this.CreateOpensshPrivateKeyWithPassword(password, opts...) +} + +// 生成公钥 pem 数据 +func (this SSH) CreatePublicKey() SSH { + return this.CreateOpensshPublicKey() +} + +// ==================== + +// 生成私钥 pem 数据 +func (this SSH) CreateOpensshPrivateKey() SSH { + if this.privateKey == nil { + err := errors.New("privateKey empty.") + return this.AppendError(err) + } + + privateBlock, err := ssh.MarshalOpenSSHPrivateKey( + rand.Reader, + this.privateKey, + this.options.Comment, + ) + if err != nil { + return this.AppendError(err) + } + + this.keyData = pem.EncodeToMemory(privateBlock) + + return this +} + +// 生成私钥带密码 pem 数据 +func (this SSH) CreateOpensshPrivateKeyWithPassword(password []byte, opts ...Opts) SSH { + if this.privateKey == nil { + err := errors.New("privateKey empty.") + return this.AppendError(err) + } + + useOpts := DefaultOpts + if len(opts) > 0 { + useOpts = opts[0] + } + + // 生成私钥 + privateBlock, err := ssh.MarshalOpenSSHPrivateKeyWithPassword( + rand.Reader, + this.privateKey, + this.options.Comment, + password, + useOpts, + ) + if err != nil { + return this.AppendError(err) + } + + this.keyData = pem.EncodeToMemory(privateBlock) + + return this +} + +// 生成公钥 pem 数据 +func (this SSH) CreateOpensshPublicKey() SSH { + if this.publicKey == nil { + err := errors.New("publicKey empty.") + return this.AppendError(err) + } + + sshPublicKey, err := ssh.NewPublicKey(this.publicKey) + if err != nil { + return this.AppendError(err) + } + + if this.options.Comment != "" { + this.keyData = ssh.MarshalAuthorizedKeyWithComment(sshPublicKey, this.options.Comment) + } else { + this.keyData = ssh.MarshalAuthorizedKey(sshPublicKey) + } + + return this +} diff --git a/cryptobin/ssh/error.go b/cryptobin/ssh/error.go new file mode 100644 index 00000000..064d3fb0 --- /dev/null +++ b/cryptobin/ssh/error.go @@ -0,0 +1,17 @@ +package ecgdsa + +import ( + "github.com/deatil/go-cryptobin/tool/errors" +) + +// 添加错误 +func (this SSH) AppendError(err ...error) SSH { + this.Errors = append(this.Errors, err...) + + return this +} + +// 获取错误 +func (this SSH) Error() error { + return errors.Join(this.Errors...) +} diff --git a/cryptobin/ssh/from.go b/cryptobin/ssh/from.go new file mode 100644 index 00000000..999b7a9b --- /dev/null +++ b/cryptobin/ssh/from.go @@ -0,0 +1,221 @@ +package ecgdsa + +import ( + "io" + "crypto/rand" + "crypto/rsa" + "crypto/dsa" + "crypto/ecdsa" + "crypto/ed25519" + + "github.com/deatil/go-cryptobin/gm/sm2" + "github.com/deatil/go-cryptobin/tool/encoding" +) + +// 生成密钥 +func (this SSH) GenerateKeyWithSeed(reader io.Reader) SSH { + switch this.options.PublicKeyType { + case KeyTypeRSA: + privateKey, err := rsa.GenerateKey(reader, this.options.Bits) + if err != nil { + return this.AppendError(err) + } + + this.privateKey = privateKey + this.publicKey = &privateKey.PublicKey + case KeyTypeDSA: + privateKey := &dsa.PrivateKey{} + dsa.GenerateParameters(&privateKey.Parameters, reader, this.options.ParameterSizes) + dsa.GenerateKey(privateKey, reader) + + this.privateKey = privateKey + this.publicKey = &privateKey.PublicKey + case KeyTypeECDSA: + privateKey, err := ecdsa.GenerateKey(this.options.Curve, reader) + if err != nil { + return this.AppendError(err) + } + + this.privateKey = privateKey + this.publicKey = &privateKey.PublicKey + case KeyTypeEdDSA: + publicKey, privateKey, err := ed25519.GenerateKey(reader) + if err != nil { + return this.AppendError(err) + } + + this.privateKey = privateKey + this.publicKey = publicKey + case KeyTypeSM2: + privateKey, err := sm2.GenerateKey(reader) + if err != nil { + return this.AppendError(err) + } + + this.privateKey = privateKey + this.publicKey = &privateKey.PublicKey + } + + return this +} + +// 使用自定义数据生成密钥对 +func GenerateKeyWithSeed(reader io.Reader, options Options) SSH { + return defaultSSH. + WithOptions(options). + GenerateKeyWithSeed(reader) +} + +// 生成密钥 +func (this SSH) GenerateKey() SSH { + return this.GenerateKeyWithSeed(rand.Reader) +} + +// 生成密钥对 +func GenerateKey(options Options) SSH { + return defaultSSH. + WithOptions(options). + GenerateKey() +} + +// ========== + +// 私钥 +func (this SSH) FromPrivateKey(key []byte) SSH { + return this.FromOpensshPrivateKey(key) +} + +// 私钥 +func FromPrivateKey(key []byte) SSH { + return defaultSSH.FromPrivateKey(key) +} + +// 私钥带密码 +func (this SSH) FromPrivateKeyWithPassword(key []byte, password []byte) SSH { + return this.FromOpensshPrivateKeyWithPassword(key, password) +} + +// 私钥带密码 +func FromPrivateKeyWithPassword(key []byte, password []byte) SSH { + return defaultSSH.FromPrivateKeyWithPassword(key, password) +} + +// 公钥 +func (this SSH) FromPublicKey(key []byte) SSH { + return defaultSSH.FromOpensshPublicKey(key) +} + +// 公钥 +func FromPublicKey(key []byte) SSH { + return defaultSSH.FromPublicKey(key) +} + +// ========== + +// 私钥 +func (this SSH) FromOpensshPrivateKey(key []byte) SSH { + privateKey, comment, err := this.ParseOpensshPrivateKeyFromPEM(key) + if err != nil { + return this.AppendError(err) + } + + this.privateKey = privateKey + this.options.Comment = comment + + return this +} + +// 私钥 +func FromOpensshPrivateKey(key []byte) SSH { + return defaultSSH.FromOpensshPrivateKey(key) +} + +// 私钥带密码 +func (this SSH) FromOpensshPrivateKeyWithPassword(key []byte, password []byte) SSH { + privateKey, comment, err := this.ParseOpensshPrivateKeyFromPEMWithPassword(key, password) + if err != nil { + return this.AppendError(err) + } + + this.privateKey = privateKey + this.options.Comment = comment + + return this +} + +// 私钥 +func FromOpensshPrivateKeyWithPassword(key []byte, password []byte) SSH { + return defaultSSH.FromOpensshPrivateKeyWithPassword(key, password) +} + +// 公钥 +func (this SSH) FromOpensshPublicKey(key []byte) SSH { + publicKey, comment, err := this.ParseOpensshPublicKeyFromPEM(key) + if err != nil { + return this.AppendError(err) + } + + this.publicKey = publicKey + this.options.Comment = comment + + return this +} + +// 公钥 +func FromOpensshPublicKey(key []byte) SSH { + return defaultSSH.FromOpensshPublicKey(key) +} + +// ========== + +// 字节 +func (this SSH) FromBytes(data []byte) SSH { + this.data = data + + return this +} + +// 字节 +func FromBytes(data []byte) SSH { + return defaultSSH.FromBytes(data) +} + +// 字符 +func (this SSH) FromString(data string) SSH { + this.data = []byte(data) + + return this +} + +// 字符 +func FromString(data string) SSH { + return defaultSSH.FromString(data) +} + +// Base64 +func (this SSH) FromBase64String(data string) SSH { + newData, err := encoding.Base64Decode(data) + + this.data = newData + + return this.AppendError(err) +} + +// Base64 +func FromBase64String(data string) SSH { + return defaultSSH.FromBase64String(data) +} + +// Hex +func (this SSH) FromHexString(data string) SSH { + newData, err := encoding.HexDecode(data) + + this.data = newData + + return this.AppendError(err) +} + +// Hex +func FromHexString(data string) SSH { + return defaultSSH.FromHexString(data) +} diff --git a/cryptobin/ssh/get.go b/cryptobin/ssh/get.go new file mode 100644 index 00000000..858a8d1c --- /dev/null +++ b/cryptobin/ssh/get.go @@ -0,0 +1,74 @@ +package ecgdsa + +import ( + "crypto" + "crypto/dsa" + "crypto/elliptic" + + "golang.org/x/crypto/ssh" +) + +// get PrivateKey +func (this SSH) GetPrivateKey() crypto.PrivateKey { + return this.privateKey +} + +// get PublicKey +func (this SSH) GetPublicKey() crypto.PublicKey { + return this.publicKey +} + +// get openssh PublicKey +func (this SSH) GetOpensshPublicKey() (ssh.PublicKey, error) { + return ssh.NewPublicKey(this.publicKey) +} + +// get Options +func (this SSH) GetOptions() Options { + return this.options +} + +// get Options Comment +func (this SSH) GetComment() string { + return this.options.Comment +} + +// get DSA ParameterSizes +func (this SSH) GetParameterSizes() dsa.ParameterSizes { + return this.options.ParameterSizes +} + +// get Options Curve +func (this SSH) GetCurve() elliptic.Curve { + return this.options.Curve +} + +// get Options Bits +func (this SSH) GetBits() int { + return this.options.Bits +} + +// get keyData +func (this SSH) GetKeyData() []byte { + return this.keyData +} + +// get data +func (this SSH) GetData() []byte { + return this.data +} + +// get parsedData +func (this SSH) GetParsedData() []byte { + return this.parsedData +} + +// get verify data +func (this SSH) GetVerify() bool { + return this.verify +} + +// get errors +func (this SSH) GetErrors() []error { + return this.Errors +} diff --git a/cryptobin/ssh/make.go b/cryptobin/ssh/make.go new file mode 100644 index 00000000..eb393ec2 --- /dev/null +++ b/cryptobin/ssh/make.go @@ -0,0 +1,36 @@ +package ecgdsa + +import( + "errors" + "crypto/rsa" + "crypto/dsa" + "crypto/ecdsa" + "crypto/ed25519" + + "github.com/deatil/go-cryptobin/gm/sm2" +) + +// 生成公钥 +func (this SSH) MakePublicKey() SSH { + this.publicKey = nil + + if this.privateKey == nil { + err := errors.New("privateKey empty.") + return this.AppendError(err) + } + + switch prikey := this.privateKey.(type) { + case *rsa.PrivateKey: + this.publicKey = &prikey.PublicKey + case *dsa.PrivateKey: + this.publicKey = &prikey.PublicKey + case *ecdsa.PrivateKey: + this.publicKey = &prikey.PublicKey + case ed25519.PrivateKey: + this.publicKey = prikey.Public().(ed25519.PublicKey) + case *sm2.PrivateKey: + this.publicKey = &prikey.PublicKey + } + + return this +} diff --git a/cryptobin/ssh/on.go b/cryptobin/ssh/on.go new file mode 100644 index 00000000..f7ea8079 --- /dev/null +++ b/cryptobin/ssh/on.go @@ -0,0 +1,14 @@ +package ecgdsa + +type ( + // 错误方法 + EcgdsaErrorFunc = func([]error) +) + +// 引出错误信息 +func (this SSH) OnError(fn EcgdsaErrorFunc) SSH { + fn(this.Errors) + + return this +} + diff --git a/cryptobin/ssh/parse.go b/cryptobin/ssh/parse.go new file mode 100644 index 00000000..cb364fea --- /dev/null +++ b/cryptobin/ssh/parse.go @@ -0,0 +1,58 @@ +package ecgdsa + +import ( + "errors" + "crypto" + "encoding/pem" + + crypto_ssh "golang.org/x/crypto/ssh" + + "github.com/deatil/go-cryptobin/ssh" +) + +var ( + ErrKeyMustBePEMEncoded = errors.New("invalid key: Key must be a PEM encoded Openssh key") + ErrNotOpensshPublicKey = errors.New("key is not a valid SSH public key") +) + +// 解析私钥 +func (this SSH) ParseOpensshPrivateKeyFromPEM(key []byte) (crypto.PrivateKey, string, error) { + // Parse PEM block + var block *pem.Block + if block, _ = pem.Decode(key); block == nil { + return nil, "", ErrKeyMustBePEMEncoded + } + + return ssh.ParseOpenSSHPrivateKey(block.Bytes) +} + +// 解析带密码的私钥 +func (this SSH) ParseOpensshPrivateKeyFromPEMWithPassword(key []byte, password []byte) (crypto.PrivateKey, string, error) { + var block *pem.Block + if block, _ = pem.Decode(key); block == nil { + return nil, "", ErrKeyMustBePEMEncoded + } + + return ssh.ParseOpenSSHPrivateKeyWithPassword(block.Bytes, password) +} + +// 解析公钥 +func (this SSH) ParseOpensshPublicKeyFromPEM(key []byte) (crypto.PublicKey, string, error) { + var err error + + // Parse the key + var parsedKey crypto_ssh.PublicKey + var comment string + if parsedKey, comment, _, _, err = ssh.ParseAuthorizedKey(key); err != nil { + return nil, "", err + } + + var pkey crypto_ssh.CryptoPublicKey + var ok bool + + if pkey, ok = parsedKey.(crypto_ssh.CryptoPublicKey); !ok { + return nil, "", ErrNotOpensshPublicKey + } + + return pkey.CryptoPublicKey(), comment, nil +} diff --git a/cryptobin/ssh/sign.go b/cryptobin/ssh/sign.go new file mode 100644 index 00000000..e73ad9b3 --- /dev/null +++ b/cryptobin/ssh/sign.go @@ -0,0 +1,78 @@ +package ecgdsa + +import ( + "errors" + "crypto/rand" + "encoding/pem" + + crypto_ssh "golang.org/x/crypto/ssh" + + "github.com/deatil/go-cryptobin/ssh" +) + +// 私钥签名 ASN1 +func (this SSH) Sign() SSH { + if this.privateKey == nil { + err := errors.New("privateKey empty.") + return this.AppendError(err) + } + + signer, err := ssh.NewSignerFromKey(this.privateKey) + if err != nil { + return this.AppendError(err) + } + + sig, err := signer.Sign(rand.Reader, this.data) + if err != nil { + return this.AppendError(err) + } + + sigBlock := &pem.Block{ + Type: "OPENSSH SIGNATURE", + Headers: map[string]string{ + "Format": sig.Format, + }, + Bytes: sig.Blob, + } + + this.parsedData = pem.EncodeToMemory(sigBlock) + + return this.AppendError(err) +} + +// 公钥验证 ASN1 +// 使用原始数据[data]对比签名后数据 +func (this SSH) Verify(data []byte) SSH { + if this.publicKey == nil { + err := errors.New("publicKey empty.") + return this.AppendError(err) + } + + block, _ := pem.Decode(this.data) + if block == nil { + err := errors.New("Signature error.") + return this.AppendError(err) + } + + format, ok := block.Headers["Format"] + if !ok { + err := errors.New("Signature Format error.") + return this.AppendError(err) + } + + pubkey, err := ssh.NewPublicKey(this.publicKey) + if err != nil { + return this.AppendError(err) + } + + err = pubkey.Verify(data, &crypto_ssh.Signature{ + Format: format, + Blob: block.Bytes, + }) + + if err == nil { + this.verify = true + } + + return this +} diff --git a/cryptobin/ssh/sign_test.go b/cryptobin/ssh/sign_test.go new file mode 100644 index 00000000..d3e9100e --- /dev/null +++ b/cryptobin/ssh/sign_test.go @@ -0,0 +1,193 @@ +package ecgdsa + +import ( + "testing" + + cryptobin_test "github.com/deatil/go-cryptobin/tool/test" +) + +var ( + prikey = ` +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtz +c2gtZWQyNTUxOQAAACADjXFGIcSTKap9+8F1Z0jH26A8WtUD9nogUD8fKgNitQAA +AIPzVi9B81YvQQAAAAtzc2gtZWQyNTUxOQAAACADjXFGIcSTKap9+8F1Z0jH26A8 +WtUD9nogUD8fKgNitQAAAEBtYVGTSuL9/OEtFMPDkvdKNpzcbliZhDiZAMr12VxO +2QONcUYhxJMpqn37wXVnSMfboDxa1QP2eiBQPx8qA2K1AAAAAA== +-----END OPENSSH PRIVATE KEY----- + ` + + pubkey = ` +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAONcUYhxJMpqn37wXVnSMfboDxa1QP2eiBQPx8qA2K1 + ` + + pubkey2 = ` +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL5YLiGEuiHIP3p8Uvz0mCQXP9YtN/TS7vUcc0D+BA76 + ` +) + +func Test_Sign(t *testing.T) { + assertNotEmpty := cryptobin_test.AssertNotEmptyT(t) + assertBool := cryptobin_test.AssertBoolT(t) + assertError := cryptobin_test.AssertErrorT(t) + + data := "test-pass" + + // 签名 + objSign := New(). + FromString(data). + FromOpensshPrivateKey([]byte(prikey)). + Sign() + signed := objSign.ToString() + + assertError(objSign.Error(), "Sign-Sign") + assertNotEmpty(signed, "Sign-Sign") + + // 验证 + objVerify := New(). + FromString(signed). + FromOpensshPublicKey([]byte(pubkey)). + Verify([]byte(data)) + + assertError(objVerify.Error(), "Sign-Verify") + assertBool(objVerify.ToVerify(), "Sign-Verify") +} + +func Test_Sign_Check(t *testing.T) { + assertBool := cryptobin_test.AssertBoolT(t) + assertError := cryptobin_test.AssertErrorT(t) + + data := "test-pass" + + signed := ` +-----BEGIN OPENSSH SIGNATURE----- +Format: ssh-ed25519 + +N85866utz9/+tNzFvpN6zjz7yMlBy9ONXBe9wKayFGykXBQVoqBKQcb+hcwPh/pA +c5pto6ZM6K74kS1PDb04CQ== +-----END OPENSSH SIGNATURE----- + ` + + // 验证 + objVerify := New(). + FromString(signed). + FromOpensshPublicKey([]byte(pubkey)). + Verify([]byte(data)) + + assertError(objVerify.Error(), "Sign-Verify") + assertBool(objVerify.ToVerify(), "Sign-Verify") +} + +func Test_CheckKeyPair(t *testing.T) { + assertBool := cryptobin_test.AssertBoolT(t) + assertError := cryptobin_test.AssertErrorT(t) + + { + obj := New(). + FromOpensshPrivateKey([]byte(prikey)). + FromOpensshPublicKey([]byte(pubkey)) + + assertError(obj.Error(), "CheckKeyPair") + assertBool(obj.CheckKeyPair(), "CheckKeyPair") + } + + { + obj := New(). + FromOpensshPrivateKey([]byte(prikey)). + FromOpensshPublicKey([]byte(pubkey2)) + + assertError(obj.Error(), "CheckKeyPair 2") + assertBool(!obj.CheckKeyPair(), "CheckKeyPair 2") + } + +} + +func Test_CheckKeyPair2(t *testing.T) { + cases := []string{ + "RSA", + "DSA", + "ECDSA", + "EdDSA", + "SM2", + } + + for _, c := range cases { + t.Run(c, func(t *testing.T) { + test_CheckKeyPair2(t, c) + }) + } +} + +func test_CheckKeyPair2(t *testing.T, keyType string) { + assertBool := cryptobin_test.AssertBoolT(t) + assertError := cryptobin_test.AssertErrorT(t) + assertNotEmpty := cryptobin_test.AssertNotEmptyT(t) + + obj0 := New().SetPublicKeyType(keyType).GenerateKey() + assertError(obj0.Error(), "test_CheckKeyPair2") + + prikey := obj0.CreateOpensshPrivateKey().ToKeyBytes() + assertNotEmpty(prikey, "test_CheckKeyPair2-PrivateKey") + + pubkey := obj0.CreateOpensshPublicKey().ToKeyBytes() + assertNotEmpty(pubkey, "test_CheckKeyPair2-PublicKey") + + obj := New(). + FromOpensshPrivateKey(prikey). + FromOpensshPublicKey(pubkey) + + assertError(obj.Error(), "test_CheckKeyPair2") + assertBool(obj.CheckKeyPair(), "test_CheckKeyPair2") +} + +func Test_Sign2(t *testing.T) { + cases := []string{ + "RSA", + "DSA", + "ECDSA", + "EdDSA", + "SM2", + } + + for _, c := range cases { + t.Run(c, func(t *testing.T) { + test_Sign2(t, c) + }) + } +} + +func test_Sign2(t *testing.T, keyType string) { + assertNotEmpty := cryptobin_test.AssertNotEmptyT(t) + assertBool := cryptobin_test.AssertBoolT(t) + assertError := cryptobin_test.AssertErrorT(t) + + data := "test-pass" + + obj0 := New().SetPublicKeyType(keyType).GenerateKey() + assertError(obj0.Error(), "test_Sign2") + + prikey := obj0.CreateOpensshPrivateKey().ToKeyBytes() + assertNotEmpty(prikey, "test_Sign2-PrivateKey") + + pubkey := obj0.CreateOpensshPublicKey().ToKeyBytes() + assertNotEmpty(pubkey, "test_Sign2-PublicKey") + + // 签名 + objSign := New(). + FromString(data). + FromOpensshPrivateKey(prikey). + Sign() + signed := objSign.ToString() + + assertError(objSign.Error(), "test_Sign2-Sign") + assertNotEmpty(signed, "test_Sign2-Sign") + + // 验证 + objVerify := New(). + FromString(signed). + FromOpensshPublicKey(pubkey). + Verify([]byte(data)) + + assertError(objVerify.Error(), "test_Sign2-Verify") + assertBool(objVerify.ToVerify(), "test_Sign2-Verify") +} diff --git a/cryptobin/ssh/ssh.go b/cryptobin/ssh/ssh.go new file mode 100644 index 00000000..be27fa56 --- /dev/null +++ b/cryptobin/ssh/ssh.go @@ -0,0 +1,110 @@ +package ecgdsa + +import ( + "strconv" + "crypto" + "crypto/dsa" + "crypto/elliptic" +) + +// public key type +type PublicKeyType uint + +func (typ PublicKeyType) String() string { + switch typ { + case KeyTypeRSA: + return "RSA" + case KeyTypeDSA: + return "DSA" + case KeyTypeECDSA: + return "ECDSA" + case KeyTypeEdDSA: + return "EdDSA" + case KeyTypeSM2: + return "SM2" + default: + return "unknown KeyType value " + strconv.Itoa(int(typ)) + } +} + +const ( + KeyTypeRSA PublicKeyType = 1 + iota + KeyTypeDSA + KeyTypeECDSA + KeyTypeEdDSA + KeyTypeSM2 +) + +// Options +type Options struct { + // public key type + PublicKeyType PublicKeyType + + // comment data + Comment string + + // DSA ParameterSizes + ParameterSizes dsa.ParameterSizes + + // ecc curve + Curve elliptic.Curve + + // generates RSA private key bit size + Bits int +} + +/** + * SSH + * + * @create 2025-1-12 + * @author deatil + */ +type SSH struct { + // PrivateKey + privateKey crypto.PrivateKey + + // PublicKey + publicKey crypto.PublicKey + + // options + options Options + + // [私钥/公钥]数据 + keyData []byte + + // 传入数据 + data []byte + + // 解析后的数据 + parsedData []byte + + // 验证结果 + verify bool + + // 错误 + Errors []error +} + +// 构造函数 +func NewSSH() SSH { + return SSH{ + options: Options{ + PublicKeyType: KeyTypeRSA, + ParameterSizes: dsa.L1024N160, + Curve: elliptic.P256(), + Bits: 2048, + }, + verify: false, + Errors: make([]error, 0), + } +} + +// 构造函数 +func New() SSH { + return NewSSH() +} + +var ( + // 默认 + defaultSSH = NewSSH() +) diff --git a/cryptobin/ssh/ssh_test.go b/cryptobin/ssh/ssh_test.go new file mode 100644 index 00000000..683104aa --- /dev/null +++ b/cryptobin/ssh/ssh_test.go @@ -0,0 +1,268 @@ +package ecgdsa + +import ( + "testing" + "crypto/dsa" + "crypto/elliptic" + + cryptobin_test "github.com/deatil/go-cryptobin/tool/test" +) + +func Test_GenKey(t *testing.T) { + cases := []string{ + "RSA", + "DSA", + "ECDSA", + "EdDSA", + "SM2", + } + + for _, c := range cases { + t.Run(c, func(t *testing.T) { + test_GenKey(t, c) + }) + } +} + +func test_GenKey(t *testing.T, keyType string) { + assertError := cryptobin_test.AssertErrorT(t) + assertEqual := cryptobin_test.AssertEqualT(t) + assertNotEmpty := cryptobin_test.AssertNotEmptyT(t) + + obj := New().SetPublicKeyType(keyType).GenerateKey() + assertError(obj.Error(), "Test_GenKey") + + { + prikey := obj.CreateOpensshPrivateKey().ToKeyBytes() + assertNotEmpty(prikey, "Test_GenKey-PrivateKey") + + pubkey := obj.CreateOpensshPublicKey().ToKeyBytes() + assertNotEmpty(pubkey, "Test_GenKey-PublicKey") + + // t.Errorf("%s, %s \n", string(prikey), string(pubkey)) + + newSSH := New().FromOpensshPrivateKey(prikey) + assertError(newSSH.Error(), "Test_GenKey-newSSH") + + assertEqual(newSSH.GetPrivateKey(), obj.GetPrivateKey(), "Test_GenKey-newSSH") + + newSSH2 := New().FromOpensshPublicKey(pubkey) + assertError(newSSH2.Error(), "Test_GenKey-newSSH2") + + assertEqual(newSSH2.GetPublicKey(), obj.GetPublicKey(), "Test_GenKey-newSSH2") + } + + { + password := []byte("test-password") + + prikey3 := obj.CreateOpensshPrivateKeyWithPassword(password).ToKeyBytes() + assertNotEmpty(prikey3, "Test_GenKey-PrivateKey 3") + + newSSH3 := New().FromOpensshPrivateKeyWithPassword(prikey3, password) + assertError(newSSH3.Error(), "Test_GenKey-newSSH3") + + assertEqual(newSSH3.GetPrivateKey(), obj.GetPrivateKey(), "Test_GenKey-newSSH3") + } +} + +func Test_GenKey2(t *testing.T) { + cases := []string{ + "RSA", + "DSA", + "ECDSA", + "EdDSA", + "SM2", + } + + for _, c := range cases { + t.Run(c, func(t *testing.T) { + test_GenKey2(t, c) + }) + } +} + +func test_GenKey2(t *testing.T, keyType string) { + assertError := cryptobin_test.AssertErrorT(t) + assertEqual := cryptobin_test.AssertEqualT(t) + assertNotEmpty := cryptobin_test.AssertNotEmptyT(t) + + obj := New().SetPublicKeyType(keyType).GenerateKey() + assertError(obj.Error(), "Test_GenKey") + + { + prikey := obj.CreatePrivateKey().ToKeyBytes() + assertNotEmpty(prikey, "Test_GenKey-PrivateKey") + + pubkey := obj.CreatePublicKey().ToKeyBytes() + assertNotEmpty(pubkey, "Test_GenKey-PublicKey") + + // t.Errorf("%s, %s \n", string(prikey), string(pubkey)) + + newSSH := New().FromPrivateKey(prikey) + assertError(newSSH.Error(), "Test_GenKey-newSSH") + + assertEqual(newSSH.GetPrivateKey(), obj.GetPrivateKey(), "Test_GenKey-newSSH") + + newSSH2 := New().FromPublicKey(pubkey) + assertError(newSSH2.Error(), "Test_GenKey-newSSH2") + + assertEqual(newSSH2.GetPublicKey(), obj.GetPublicKey(), "Test_GenKey-newSSH2") + } + + { + password := []byte("test-password") + + prikey3 := obj.CreatePrivateKeyWithPassword(password).ToKeyBytes() + assertNotEmpty(prikey3, "Test_GenKey-PrivateKey 3") + + newSSH3 := New().FromPrivateKeyWithPassword(prikey3, password) + assertError(newSSH3.Error(), "Test_GenKey-newSSH3") + + assertEqual(newSSH3.GetPrivateKey(), obj.GetPrivateKey(), "Test_GenKey-newSSH3") + } +} + +func Test_GenKey3(t *testing.T) { + cases := []PublicKeyType{ + KeyTypeRSA, + KeyTypeDSA, + KeyTypeECDSA, + KeyTypeEdDSA, + KeyTypeSM2, + } + + for _, c := range cases { + t.Run(c.String(), func(t *testing.T) { + test_GenKey3(t, c) + }) + } +} + +func test_GenKey3(t *testing.T, keyType PublicKeyType) { + assertError := cryptobin_test.AssertErrorT(t) + assertEqual := cryptobin_test.AssertEqualT(t) + assertNotEmpty := cryptobin_test.AssertNotEmptyT(t) + + newOpts := func(ktype PublicKeyType) Options { + opt := Options{ + PublicKeyType: ktype, + ParameterSizes: dsa.L1024N160, + Curve: elliptic.P256(), + Bits: 2048, + } + + return opt + } + + obj := GenerateKey(newOpts(keyType)) + assertError(obj.Error(), "Test_GenKey") + + { + prikey := obj.CreatePrivateKey().ToKeyBytes() + assertNotEmpty(prikey, "Test_GenKey-PrivateKey") + + pubkey := obj.CreatePublicKey().ToKeyBytes() + assertNotEmpty(pubkey, "Test_GenKey-PublicKey") + + // t.Errorf("%s, %s \n", string(prikey), string(pubkey)) + + newSSH := New().FromPrivateKey(prikey) + assertError(newSSH.Error(), "Test_GenKey-newSSH") + + assertEqual(newSSH.GetPrivateKey(), obj.GetPrivateKey(), "Test_GenKey-newSSH") + + newSSH2 := New().FromPublicKey(pubkey) + assertError(newSSH2.Error(), "Test_GenKey-newSSH2") + + assertEqual(newSSH2.GetPublicKey(), obj.GetPublicKey(), "Test_GenKey-newSSH2") + } + + { + password := []byte("test-password") + + prikey3 := obj.CreatePrivateKeyWithPassword(password).ToKeyBytes() + assertNotEmpty(prikey3, "Test_GenKey-PrivateKey 3") + + newSSH3 := New().FromPrivateKeyWithPassword(prikey3, password) + assertError(newSSH3.Error(), "Test_GenKey-newSSH3") + + assertEqual(newSSH3.GetPrivateKey(), obj.GetPrivateKey(), "Test_GenKey-newSSH3") + } +} + +func Test_GenKey_ECDSA(t *testing.T) { + cases := []string{ + "P256", + "P384", + "P521", + } + + for _, c := range cases { + t.Run(c, func(t *testing.T) { + test_GenKey_ECDSA(t, c) + }) + } +} + +func test_GenKey_ECDSA(t *testing.T, curve string) { + assertError := cryptobin_test.AssertErrorT(t) + assertEqual := cryptobin_test.AssertEqualT(t) + assertNotEmpty := cryptobin_test.AssertNotEmptyT(t) + + obj := New(). + SetPublicKeyType("ECDSA"). + SetCurve(curve). + GenerateKey() + assertError(obj.Error(), "Test_GenKey_ECDSA") + + prikey := obj.CreatePrivateKey().ToKeyBytes() + assertNotEmpty(prikey, "Test_GenKey_ECDSA-PrivateKey") + + pubkey := obj.CreatePublicKey().ToKeyBytes() + assertNotEmpty(pubkey, "Test_GenKey_ECDSA-PublicKey") + + newSSH := New().FromPrivateKey(prikey) + assertError(newSSH.Error(), "Test_GenKey_ECDSA-newSSH") + + assertEqual(newSSH.GetPrivateKey(), obj.GetPrivateKey(), "Test_GenKey_ECDSA-newSSH") + + newSSH2 := New().FromPublicKey(pubkey) + assertError(newSSH2.Error(), "Test_GenKey_ECDSA-newSSH2") + + assertEqual(newSSH2.GetPublicKey(), obj.GetPublicKey(), "Test_GenKey_ECDSA-newSSH2") +} + +func Test_GenKey_ECDSA_With_Comment(t *testing.T) { + assertError := cryptobin_test.AssertErrorT(t) + assertEqual := cryptobin_test.AssertEqualT(t) + assertNotEmpty := cryptobin_test.AssertNotEmptyT(t) + + comment := "test-comment" + + obj := New(). + SetPublicKeyType("ECDSA"). + GenerateKey() + assertError(obj.Error(), "Test_GenKey_ECDSA_With_Comment") + + prikey := obj.WithComment(comment). + CreatePrivateKey(). + ToKeyBytes() + assertNotEmpty(prikey, "Test_GenKey_ECDSA_With_Comment-PrivateKey") + + pubkey := obj.WithComment(comment). + CreatePublicKey(). + ToKeyBytes() + assertNotEmpty(pubkey, "Test_GenKey_ECDSA_With_Comment-PublicKey") + + newSSH := New().FromPrivateKey(prikey) + assertError(newSSH.Error(), "Test_GenKey_ECDSA_With_Comment-newSSH") + + assertEqual(newSSH.GetPrivateKey(), obj.GetPrivateKey(), "Test_GenKey_ECDSA_With_Comment-newSSH") + assertEqual(newSSH.GetComment(), comment, "Test_GenKey_ECDSA_With_Comment-newSSH-comment") + + newSSH2 := New().FromPublicKey(pubkey) + assertError(newSSH2.Error(), "Test_GenKey_ECDSA_With_Comment-newSSH2") + + assertEqual(newSSH2.GetPublicKey(), obj.GetPublicKey(), "Test_GenKey_ECDSA_With_Comment-newSSH2") + assertEqual(newSSH2.GetComment(), comment, "Test_GenKey_ECDSA_With_Comment-newSSH2-comment") +} diff --git a/cryptobin/ssh/to.go b/cryptobin/ssh/to.go new file mode 100644 index 00000000..c50014a1 --- /dev/null +++ b/cryptobin/ssh/to.go @@ -0,0 +1,53 @@ +package ecgdsa + +import ( + "github.com/deatil/go-cryptobin/tool/encoding" +) + +// 私钥/公钥 +func (this SSH) ToKeyBytes() []byte { + return this.keyData +} + +// 私钥/公钥 +func (this SSH) ToKeyString() string { + return string(this.keyData) +} + +// ========== + +// 输出字节 +func (this SSH) ToBytes() []byte { + return this.parsedData +} + +// 输出字符 +func (this SSH) ToString() string { + return string(this.parsedData) +} + +// 输出Base64 +func (this SSH) ToBase64String() string { + return encoding.Base64Encode(this.parsedData) +} + +// 输出Hex +func (this SSH) ToHexString() string { + return encoding.HexEncode(this.parsedData) +} + +// ========== + +// 验证结果 +func (this SSH) ToVerify() bool { + return this.verify +} + +// 验证结果,返回 int 类型 +func (this SSH) ToVerifyInt() int { + if this.verify { + return 1 + } + + return 0 +} diff --git a/cryptobin/ssh/with.go b/cryptobin/ssh/with.go new file mode 100644 index 00000000..03f3208d --- /dev/null +++ b/cryptobin/ssh/with.go @@ -0,0 +1,165 @@ +package ecgdsa + +import ( + "crypto" + "crypto/dsa" + "crypto/elliptic" + + "golang.org/x/crypto/ssh" +) + +// 设置 PrivateKey +func (this SSH) WithPrivateKey(key crypto.PrivateKey) SSH { + this.privateKey = key + + return this +} + +// 设置 PublicKey +func (this SSH) WithPublicKey(key crypto.PublicKey) SSH { + this.publicKey = key + + return this +} + +// 设置 openssh PublicKey +func (this SSH) SetOpensshPublicKey(key ssh.PublicKey) SSH { + publicKey, err := ssh.NewPublicKey(key) + if err != nil { + return this.AppendError(err) + } + + this.publicKey = publicKey + + return this +} + +// 设置配置 +func (this SSH) WithOptions(options Options) SSH { + this.options = options + + return this +} + +// public key type +func (this SSH) WithPublicKeyType(keyType PublicKeyType) SSH { + this.options.PublicKeyType = keyType + + return this +} + +// public key type +// 可选参数: +// [ RSA | DSA | ECDSA | EdDSA | SM2 ] +func (this SSH) SetPublicKeyType(keyType string) SSH { + switch keyType { + case "RSA": + this.options.PublicKeyType = KeyTypeRSA + case "DSA": + this.options.PublicKeyType = KeyTypeDSA + case "ECDSA": + this.options.PublicKeyType = KeyTypeECDSA + case "EdDSA": + this.options.PublicKeyType = KeyTypeEdDSA + case "SM2": + this.options.PublicKeyType = KeyTypeSM2 + } + + return this +} + +// 设置注释 +func (this SSH) WithComment(comment string) SSH { + this.options.Comment = comment + + return this +} + +// 设置 DSA ParameterSizes +func (this SSH) WithParameterSizes(sizes dsa.ParameterSizes) SSH { + this.options.ParameterSizes = sizes + + return this +} + +// 设置 DSA ParameterSizes +// 可选参数: +// [ L1024N160 | L2048N224 | L2048N256 | L3072N256 ] +func (this SSH) SetParameterSizes(ln string) SSH { + switch ln { + case "L1024N160": + this.options.ParameterSizes = dsa.L1024N160 + case "L2048N224": + this.options.ParameterSizes = dsa.L2048N224 + case "L2048N256": + this.options.ParameterSizes = dsa.L2048N256 + case "L3072N256": + this.options.ParameterSizes = dsa.L3072N256 + } + + return this +} + +// 设置曲线类型 +func (this SSH) WithCurve(curve elliptic.Curve) SSH { + this.options.Curve = curve + + return this +} + +// 设置曲线类型 +// 可选参数: [ P521 | P384 | P256 ] +func (this SSH) SetCurve(curve string) SSH { + switch curve { + case "P256": + this.options.Curve = elliptic.P256() + case "P384": + this.options.Curve = elliptic.P384() + case "P521": + this.options.Curve = elliptic.P521() + } + + return this +} + +// RSA private key bit size +func (this SSH) WithBits(bits int) SSH { + this.options.Bits = bits + + return this +} + +// 设置 keyData +func (this SSH) WithKeyData(data []byte) SSH { + this.keyData = data + + return this +} + +// 设置 data +func (this SSH) WithData(data []byte) SSH { + this.data = data + + return this +} + +// 设置 parsedData +func (this SSH) WithParsedData(data []byte) SSH { + this.parsedData = data + + return this +} + +// 设置验证结果 +func (this SSH) WithVerify(data bool) SSH { + this.verify = data + + return this +} + +// 设置错误 +func (this SSH) WithErrors(errs []error) SSH { + this.Errors = errs + + return this +} diff --git a/go.mod b/go.mod index 0d9ae5a1..35fc17c8 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,8 @@ module github.com/deatil/go-cryptobin go 1.20 require ( - golang.org/x/crypto v0.24.0 - golang.org/x/text v0.16.0 + golang.org/x/crypto v0.31.0 + golang.org/x/text v0.21.0 ) -require golang.org/x/sys v0.21.0 // indirect +require golang.org/x/sys v0.28.0 // indirect diff --git a/ssh/key_dsa.go b/ssh/key_dsa.go new file mode 100644 index 00000000..eb41e2b6 --- /dev/null +++ b/ssh/key_dsa.go @@ -0,0 +1,89 @@ +package ssh + +import ( + "fmt" + "errors" + "math/big" + "crypto" + "crypto/dsa" + + "golang.org/x/crypto/ssh" +) + +// DSA key +type KeyDSA struct {} + +// Marshal key +func (this KeyDSA) Marshal(key crypto.PrivateKey, comment string) (string, []byte, []byte, error) { + k, ok := key.(*dsa.PrivateKey) + if !ok { + return "", nil, nil, errors.New(fmt.Sprintf("unsupported key type %T", key)) + } + + keyType := ssh.KeyAlgoDSA + + pubKey := struct { + KeyType string + P, Q, G *big.Int + Y *big.Int + }{ + keyType, + k.PublicKey.P, + k.PublicKey.Q, + k.PublicKey.G, + k.PublicKey.Y, + } + pubkey := ssh.Marshal(pubKey) + + // Marshal private key. + prikey := struct { + P, Q, G *big.Int + Y *big.Int + X *big.Int + Comment string + }{ + k.PublicKey.P, + k.PublicKey.Q, + k.PublicKey.G, + k.PublicKey.Y, + k.X, + comment, + } + rest := ssh.Marshal(prikey) + + return keyType, pubkey, rest, nil +} + +// Parse key +func (this KeyDSA) Parse(rest []byte) (crypto.PrivateKey, string, error) { + // https://github.com/openssh/openssh-portable/blob/master/sshkey.c + key := struct { + P, Q, G *big.Int + Y *big.Int + X *big.Int + Comment string + Pad []byte `ssh:"rest"` + }{} + + if err := ssh.Unmarshal(rest, &key); err != nil { + return nil, "", err + } + + if err := checkOpenSSHKeyPadding(key.Pad); err != nil { + return nil, "", err + } + + pk := &dsa.PrivateKey{ + PublicKey: dsa.PublicKey{ + Parameters: dsa.Parameters{ + P: key.P, + Q: key.Q, + G: key.G, + }, + Y: key.Y, + }, + X: key.X, + } + + return pk, key.Comment, nil +} diff --git a/ssh/key_rsa.go b/ssh/key_rsa.go index 1649775f..833ca61d 100644 --- a/ssh/key_rsa.go +++ b/ssh/key_rsa.go @@ -46,7 +46,8 @@ func (this KeyRSA) Marshal(key crypto.PrivateKey, comment string) (string, []byt Comment string }{ k.PublicKey.N, E, - k.D, k.Precomputed.Qinv, k.Primes[0], k.Primes[1], + k.D, k.Precomputed.Qinv, + k.Primes[0], k.Primes[1], comment, } rest := ssh.Marshal(prikey) diff --git a/ssh/key_setting.go b/ssh/key_setting.go index 8c12b291..3ef71ac4 100644 --- a/ssh/key_setting.go +++ b/ssh/key_setting.go @@ -2,6 +2,7 @@ package ssh import ( "crypto/rsa" + "crypto/dsa" "crypto/ecdsa" "crypto/ed25519" @@ -18,6 +19,13 @@ func init() { return new(KeyRSA) }) + AddKey(GetStructName(&dsa.PrivateKey{}), func() Key { + return new(KeyDSA) + }) + AddKey(ssh.KeyAlgoDSA, func() Key { + return new(KeyDSA) + }) + AddKey(GetStructName(&ecdsa.PrivateKey{}), func() Key { return new(KeyECDSA) }) diff --git a/ssh/ssh_test.go b/ssh/ssh_test.go index bdea88e2..0cc7e740 100644 --- a/ssh/ssh_test.go +++ b/ssh/ssh_test.go @@ -7,6 +7,7 @@ import ( "testing" "encoding/pem" "crypto/rsa" + "crypto/dsa" "crypto/rand" "crypto/ecdsa" "crypto/ed25519" @@ -333,3 +334,25 @@ func Test_ParseSSHKey_SM2(t *testing.T) { assertEqual(sshComment, "test-ssh", "Test_ParseSSHKey_SM2") } + +func Test_ParseSSHKey_DSA(t *testing.T) { + assertEqual := cryptobin_test.AssertEqualT(t) + assertError := cryptobin_test.AssertErrorT(t) + assertNotEmpty := cryptobin_test.AssertNotEmptyT(t) + + privateKey := &dsa.PrivateKey{} + dsa.GenerateParameters(&privateKey.Parameters, rand.Reader, dsa.L2048N224) + dsa.GenerateKey(privateKey, rand.Reader) + + block, err := MarshalOpenSSHPrivateKey(rand.Reader, privateKey, "test-ssh") + assertError(err, "Test_ParseSSHKey_DSA-Marshal") + + blockkeyData := pem.EncodeToMemory(block) + + sshkeyName, sshComment, err := testParseSSHKey(string(blockkeyData), "") + assertError(err, "Test_ParseSSHKey_DSA") + assertNotEmpty(sshkeyName, "Test_ParseSSHKey_DSA-sshkeyName") + assertNotEmpty(sshComment, "Test_ParseSSHKey_DSA-sshComment") + + assertEqual(sshComment, "test-ssh", "Test_ParseSSHKey_DSA") +}