-
-
Notifications
You must be signed in to change notification settings - Fork 5.9k
Allow pushmirror to use publickey authentication #18835
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
eefa9e5
81d5e26
4e1731a
560494a
0631c76
f8b28db
3dcf98e
17e6ae8
25bd4db
c627b15
37f5359
ed6e4f8
7acceb2
b2653cd
af81b21
6bcd6ba
0328a48
f50e3df
4f22413
6bbb821
475ae03
084126b
409945a
22e7162
e562702
d46e666
dc80fdf
4adf2e1
b57c09b
08be2f2
41dcf87
a296667
ceaf8dd
8892708
435ce88
23ec50e
05f5eb0
2a0d8f0
81d7346
3961dcb
26a486c
b4f56b9
801741d
0c0ffff
15193cd
7be530f
f51832b
86fec2d
e110c67
1516cab
a83a75c
ac5cf7a
5162525
d85d495
81e1e96
e129fb5
2622046
9064406
ba1e6a1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
// Copyright 2022 The Gitea Authors. All rights reserved. | ||
// Use of this source code is governed by a MIT-style | ||
// license that can be found in the LICENSE file. | ||
|
||
package migrations | ||
|
||
import ( | ||
"time" | ||
|
||
"code.gitea.io/gitea/models/repo" | ||
"code.gitea.io/gitea/modules/timeutil" | ||
"xorm.io/xorm" | ||
) | ||
|
||
func addKeypairToPushMirror(x *xorm.Engine) error { | ||
type PushMirror struct { | ||
ID int64 `xorm:"pk autoincr"` | ||
RepoID int64 `xorm:"INDEX"` | ||
Repo *repo.Repository `xorm:"-"` | ||
RemoteName string | ||
|
||
// A keypair formatted in OpenSSH format. | ||
PublicKey string | ||
PrivateKey string `xorm:"VARCHAR(400)"` | ||
|
||
SyncOnCommit bool `xorm:"NOT NULL DEFAULT true"` | ||
Interval time.Duration | ||
CreatedUnix timeutil.TimeStamp `xorm:"created"` | ||
LastUpdateUnix timeutil.TimeStamp `xorm:"INDEX last_update"` | ||
LastError string `xorm:"text"` | ||
} | ||
|
||
return x.Sync2(new(PushMirror)) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,7 @@ package repo | |
import ( | ||
"context" | ||
"errors" | ||
"strings" | ||
"time" | ||
|
||
"code.gitea.io/gitea/models/db" | ||
|
@@ -26,6 +27,10 @@ type PushMirror struct { | |
Repo *Repository `xorm:"-"` | ||
RemoteName string | ||
|
||
// A keypair formatted in OpenSSH format. | ||
PublicKey string | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will be a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I once run a script with the old code in Public keys are: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Might still want to enlarge it now to accomodate future key formats. There's no need to define database fields so tightly. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not able to guess future key formats. I prefer to just limit it to Ed25519 signatures and only add other signatures scheme if there's a need for them. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we enlarge now, it saves us from the pain of a future migration. So, unless this is a critical performance code path where there are indexing benefits by using the smaller type, I would enlarge. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Alright then, I give in. What would be a good size to use here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Correct, MySQL |
||
PrivateKey string `xorm:"VARCHAR(400)"` | ||
|
||
SyncOnCommit bool `xorm:"NOT NULL DEFAULT true"` | ||
Interval time.Duration | ||
CreatedUnix timeutil.TimeStamp `xorm:"created"` | ||
|
@@ -74,6 +79,12 @@ func (m *PushMirror) GetRemoteName() string { | |
return m.RemoteName | ||
} | ||
|
||
// GetPublicKey returns a sanitized version of the public key. | ||
// This should only be used when displaying the public key to the user, not for actual code. | ||
func (m *PushMirror) GetPublicKey() string { | ||
return strings.TrimSuffix(m.PublicKey, "\n") | ||
} | ||
|
||
// InsertPushMirror inserts a push-mirror to database | ||
func InsertPushMirror(ctx context.Context, m *PushMirror) error { | ||
_, err := db.GetEngine(ctx).Insert(m) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
// Copyright 2022 The Gitea Authors. All rights reserved. | ||
// Use of this source code is governed by a MIT-style | ||
// license that can be found in the LICENSE file. | ||
|
||
package crypto | ||
|
||
import ( | ||
"crypto/rand" | ||
"encoding/binary" | ||
"encoding/pem" | ||
"fmt" | ||
|
||
"golang.org/x/crypto/ed25519" | ||
"golang.org/x/crypto/ssh" | ||
) | ||
|
||
// GenerateEd25519Keypair generates a new public and private key from the 25519 curve. | ||
func GenerateEd25519Keypair() (publicKey, privateKey []byte, err error) { | ||
// Generate the private key from ed25519. | ||
public, private, err := ed25519.GenerateKey(nil) | ||
if err != nil { | ||
return nil, nil, fmt.Errorf("ed25519.GenerateKey: %v", err) | ||
} | ||
|
||
// Marshal the privateKey into the OpenSSH format. | ||
privPEM, err := marshalPrivateKey(private) | ||
if err != nil { | ||
return nil, nil, fmt.Errorf("not able to marshal private key into OpenSSH format: %v", err) | ||
} | ||
|
||
sshPublicKey, err := ssh.NewPublicKey(public) | ||
if err != nil { | ||
return nil, nil, fmt.Errorf("not able to create new SSH public key: %v", err) | ||
} | ||
|
||
return ssh.MarshalAuthorizedKey(sshPublicKey), pem.EncodeToMemory(privPEM), nil | ||
} | ||
|
||
// openSSHMagic contains the magic bytes, which is used to indicate it's a v1 | ||
// OpenSSH key format. "openssh-key-v1\x00" in bytes. | ||
const openSSHMagic = "openssh-key-v1\x00" | ||
|
||
// MarshalPrivateKey returns a PEM block with the private key serialized in the | ||
// OpenSSH format. | ||
// Adopted from: https://go-review.googlesource.com/c/crypto/+/218620/ | ||
func marshalPrivateKey(key ed25519.PrivateKey) (*pem.Block, error) { | ||
// The ed25519.PrivateKey is a []byte (Seed, Public) | ||
|
||
// Split the provided key in to a public key and private key bytes. | ||
publicKeyBytes := make([]byte, ed25519.PublicKeySize) | ||
privateKeyBytes := make([]byte, ed25519.PrivateKeySize) | ||
copy(publicKeyBytes, key[ed25519.SeedSize:]) | ||
copy(privateKeyBytes, key) | ||
|
||
// Now we want to eventually marshal the sshPrivateKeyStruct below but ssh.Marshal doesn't allow submarshalling | ||
// So we need to create a number of structs in order to marshal them and build the struct we need. | ||
// | ||
// 1. Create a struct that holds the public key for this private key | ||
pubKeyStruct := struct { | ||
KeyType string | ||
Pub []byte | ||
}{ | ||
KeyType: ssh.KeyAlgoED25519, | ||
Pub: publicKeyBytes, | ||
} | ||
|
||
// 2. Create a struct to contain the privateKeyBlock | ||
// 2a. Marshal keypair as the rest struct | ||
restStruct := struct { | ||
Pub []byte | ||
Priv []byte | ||
Comment string | ||
}{ | ||
publicKeyBytes, privateKeyBytes, "", | ||
} | ||
// 2b. Generate a random uint32 number. | ||
// These can be random bytes or anything else, as long it's the same. | ||
// See: https://github.com/openssh/openssh-portable/blob/f7fc6a43f1173e8b2c38770bf6cee485a562d03b/sshkey.c#L4228-L4235 | ||
var check uint32 | ||
if err := binary.Read(rand.Reader, binary.BigEndian, &check); err != nil { | ||
return nil, err | ||
} | ||
|
||
// 2c. Create the privateKeyBlock struct | ||
privateKeyBlockStruct := struct { | ||
Check1 uint32 | ||
Check2 uint32 | ||
Keytype string | ||
Rest []byte `ssh:"rest"` | ||
}{ | ||
Check1: check, | ||
Check2: check, | ||
Keytype: ssh.KeyAlgoED25519, | ||
Rest: ssh.Marshal(restStruct), | ||
} | ||
|
||
// 3. Now we're finally ready to create the OpenSSH sshPrivateKey | ||
// Head struct of the OpenSSH format. | ||
sshPrivateKeyStruct := struct { | ||
CipherName string | ||
KdfName string | ||
KdfOpts string | ||
NumKeys uint32 | ||
PubKey []byte // See pubKey | ||
PrivKeyBlock []byte // See KeyPair | ||
}{ | ||
CipherName: "none", // This is not a password protected key | ||
KdfName: "none", // so these fields are left as none and empty | ||
KdfOpts: "", // | ||
NumKeys: 1, | ||
PubKey: ssh.Marshal(pubKeyStruct), | ||
PrivKeyBlock: generateOpenSSHPadding(ssh.Marshal(privateKeyBlockStruct)), | ||
} | ||
|
||
// 4. Finally marshal the sshPrivateKeyStruct struct. | ||
bs := ssh.Marshal(sshPrivateKeyStruct) | ||
block := &pem.Block{ | ||
Type: "OPENSSH PRIVATE KEY", | ||
Bytes: append([]byte(openSSHMagic), bs...), | ||
} | ||
|
||
return block, nil | ||
} | ||
|
||
// generateOpenSSHPaddins converts the block to | ||
// accomplish a block size of 8 bytes. | ||
func generateOpenSSHPadding(block []byte) []byte { | ||
padding := []byte{1, 2, 3, 4, 5, 6, 7} | ||
|
||
mod8 := len(block) % 8 | ||
if mod8 > 0 { | ||
block = append(block, padding[:8-mod8]...) | ||
} | ||
|
||
return block | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
// Copyright 2022 The Gitea Authors. All rights reserved. | ||
// Use of this source code is governed by a MIT-style | ||
// license that can be found in the LICENSE file. | ||
|
||
package crypto | ||
|
||
import ( | ||
"bytes" | ||
"crypto/rand" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
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----- | ||
` | ||
) | ||
|
||
func TestGeneratingEd25519Keypair(t *testing.T) { | ||
// Temp override the rand.Reader for deterministic testing. | ||
oldReader := rand.Reader | ||
defer func() { | ||
rand.Reader = oldReader | ||
}() | ||
|
||
// 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 := GenerateEd25519Keypair() | ||
assert.NoError(t, err) | ||
assert.EqualValues(t, testPublicKey, string(publicKey)) | ||
assert.EqualValues(t, testPrivateKey, string(privateKey)) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -61,6 +61,10 @@ func endpointFromURL(rawurl string) *url.URL { | |
case "git": | ||
u.Scheme = "https" | ||
return u | ||
case "ssh": | ||
u.Scheme = "https" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is that really intended? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So, just to recap: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it's going to be a problem with any repository that has LFS. As pushing the LFS data over HTTP without any auth will just fail... I'm not sure if I want to implement SSH client here(as I know nothing about git-LFS and git + ssh is on it's own already tedious for non-interactive usages). Might just force to ask credentials when LFS is enabled on the repository |
||
u.User = nil | ||
return u | ||
case "file": | ||
return u | ||
default: | ||
|
Uh oh!
There was an error while loading. Please reload this page.