mirror of
https://github.com/drakkan/sftpgo.git
synced 2025-12-06 06:10:54 +03:00
sftpd: add support for OpenPubkey SSH
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
@@ -278,6 +278,8 @@ func Init() {
|
|||||||
PublicKeyAlgorithms: []string{},
|
PublicKeyAlgorithms: []string{},
|
||||||
TrustedUserCAKeys: []string{},
|
TrustedUserCAKeys: []string{},
|
||||||
RevokedUserCertsFile: "",
|
RevokedUserCertsFile: "",
|
||||||
|
OPKSSHPath: "",
|
||||||
|
OPKSSHChecksum: "",
|
||||||
LoginBannerFile: "",
|
LoginBannerFile: "",
|
||||||
EnabledSSHCommands: []string{},
|
EnabledSSHCommands: []string{},
|
||||||
KeyboardInteractiveAuthentication: true,
|
KeyboardInteractiveAuthentication: true,
|
||||||
@@ -2095,6 +2097,8 @@ func setViperDefaults() {
|
|||||||
viper.SetDefault("sftpd.public_key_algorithms", globalConf.SFTPD.PublicKeyAlgorithms)
|
viper.SetDefault("sftpd.public_key_algorithms", globalConf.SFTPD.PublicKeyAlgorithms)
|
||||||
viper.SetDefault("sftpd.trusted_user_ca_keys", globalConf.SFTPD.TrustedUserCAKeys)
|
viper.SetDefault("sftpd.trusted_user_ca_keys", globalConf.SFTPD.TrustedUserCAKeys)
|
||||||
viper.SetDefault("sftpd.revoked_user_certs_file", globalConf.SFTPD.RevokedUserCertsFile)
|
viper.SetDefault("sftpd.revoked_user_certs_file", globalConf.SFTPD.RevokedUserCertsFile)
|
||||||
|
viper.SetDefault("sftpd.opkssh_path", globalConf.SFTPD.OPKSSHPath)
|
||||||
|
viper.SetDefault("sftpd.opkssh_checksum", globalConf.SFTPD.OPKSSHChecksum)
|
||||||
viper.SetDefault("sftpd.login_banner_file", globalConf.SFTPD.LoginBannerFile)
|
viper.SetDefault("sftpd.login_banner_file", globalConf.SFTPD.LoginBannerFile)
|
||||||
viper.SetDefault("sftpd.enabled_ssh_commands", sftpd.GetDefaultSSHCommands())
|
viper.SetDefault("sftpd.enabled_ssh_commands", sftpd.GetDefaultSSHCommands())
|
||||||
viper.SetDefault("sftpd.keyboard_interactive_authentication", globalConf.SFTPD.KeyboardInteractiveAuthentication)
|
viper.SetDefault("sftpd.keyboard_interactive_authentication", globalConf.SFTPD.KeyboardInteractiveAuthentication)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ package sftpd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -1818,6 +1819,7 @@ func TestCanReadSymlink(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAuthenticationErrors(t *testing.T) {
|
func TestAuthenticationErrors(t *testing.T) {
|
||||||
|
sftpAuthError := newAuthenticationError(nil, "", "")
|
||||||
loginMethod := dataprovider.SSHLoginMethodPassword
|
loginMethod := dataprovider.SSHLoginMethodPassword
|
||||||
username := "test user"
|
username := "test user"
|
||||||
err := newAuthenticationError(fmt.Errorf("cannot validate credentials: %w", util.NewRecordNotFoundError("not found")),
|
err := newAuthenticationError(fmt.Errorf("cannot validate credentials: %w", util.NewRecordNotFoundError("not found")),
|
||||||
@@ -1842,3 +1844,42 @@ func TestAuthenticationErrors(t *testing.T) {
|
|||||||
assert.ErrorIs(t, err, sftpAuthError)
|
assert.ErrorIs(t, err, sftpAuthError)
|
||||||
assert.NotErrorIs(t, err, util.ErrNotFound)
|
assert.NotErrorIs(t, err, util.ErrNotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type mockCommandExecutor struct {
|
||||||
|
Output []byte
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f mockCommandExecutor) CombinedOutput(ctx context.Context, name string, args ...string) ([]byte, error) {
|
||||||
|
return f.Output, f.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVerifyWithOPKSSH(t *testing.T) {
|
||||||
|
sshCert := []byte(`ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAg4+hKHVPKv183MU/Q7XD/mzDBFSc2YY3eraltxLMGJo0AAAADAQABAAABAQCe6jMoy1xCQgiZkZJ7gi6NLj4uRqz2OaUGK/OJYZTfBqK+SlS9iymAluHu9K+cc4+0qxx0gn7dRTJWINSgzvca6ayYe995EKgD1hE5krh9BH0bRrXB+hGqyslcZOgLNO+v8jYojClQbRtET2tS+xb4k33GCuL5wgla2790ZgOQgs7huQUjG0S8c1W+EYt6fI4cWE/DeEBnv9sqryS8rOb0PbM6WUd7XBadwySFWYQUX0ei56GNt12Z4gADEGlFQV/OnV0PvnTcAMGUl0rfToPgJ4jgogWKoTVWuZ9wyA/x+2LRLRvgm2a969ig937/AH0i0Wq+FzqfK7EXQ99Yf5K/AAAAAAAAAAAAAAACAAAAFGhvc3QuZXhhbXBsZS5jb20ta2V5AAAAFAAAABBob3N0LmV4YW1wbGUuY29tAAAAAGXEzYAAAAAAd8sP4wAAAAAAAAAAAAAAAAAAARcAAAAHc3NoLXJzYQAAAAMBAAEAAAEBAL4PXUPSERufZWCW/hhEnylk3IeMgaa+2HcNY5Cur77a8fYy6OYZAPF+vhJUT0akwGUpTeXAZumAgHECDrJlw1J+jo9ZVT0AKDo0wU77IzNzYxob7+dpB02NJ7DLAXmPauQ07Zc5pWJFVKtmuh7YH9pjYtNXSMOXye7k06PBGzX+ztIt7nPWvD9fR2mZeTSoljeBCGZHwdlnV2ESQlQbBoEI93RPxqxJh/UCDatQPhpDbyverr2ZvB9Y45rqsx6ZVmu5RXl3MfBU1U21W/4ia2di3PybyD4rSmVoam0efcqxo6cBKSHe26OFoTuS9zgdH0iCWL37vqOFmJ7eH91M3nMAAAEUAAAADHJzYS1zaGEyLTI1NgAAAQA/ByIegNZYJRRl413S/8LxGvTZnbxsPwaluoJ/54niGZV9P28THz7d9jXfSHPjalhH93jNPfTYXvI4opnDC37ua1Nu8KKfk40IWXnnDdZLWraUxEidIzhmfVtz8kGdGoFQ8H0EzubL7zKNOTlfSfOoDlmQVOuxT/+eh2mEp4ri0/+8J1mLfLBr8tREX0/iaNjK+RKdcyTMicKursAYMCDdu8vlaphxea+ocyHM9izSX/l33t44V13ueTqIOh2Zbl2UE2k+jk+0dc1CmV0SEoiWiIyt8TRM4yQry1vPlQLsrf28sYM/QMwnhCVhyZO3vs5F25aQWrB9d51VEzBW9/fd host.example.com`)
|
||||||
|
key, _, _, _, err := ssh.ParseAuthorizedKey(sshCert) //nolint:dogsled
|
||||||
|
require.NoError(t, err)
|
||||||
|
cert, ok := key.(*ssh.Certificate)
|
||||||
|
require.True(t, ok)
|
||||||
|
c := Configuration{}
|
||||||
|
c.executor = mockCommandExecutor{
|
||||||
|
Err: errors.New("test error"),
|
||||||
|
}
|
||||||
|
err = c.verifyWithOPKSSH("user", cert)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
c.executor = mockCommandExecutor{}
|
||||||
|
err = c.verifyWithOPKSSH("", cert)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
c.executor = mockCommandExecutor{
|
||||||
|
Output: ssh.MarshalAuthorizedKey(cert),
|
||||||
|
}
|
||||||
|
err = c.verifyWithOPKSSH("", cert)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
c.executor = mockCommandExecutor{
|
||||||
|
Output: ssh.MarshalAuthorizedKey(cert.SignatureKey),
|
||||||
|
}
|
||||||
|
err = c.verifyWithOPKSSH("", cert)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ package sftpd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
@@ -25,6 +27,7 @@ import (
|
|||||||
"maps"
|
"maps"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
"slices"
|
"slices"
|
||||||
@@ -82,10 +85,20 @@ var (
|
|||||||
revokedCertManager = revokedCertificates{
|
revokedCertManager = revokedCertificates{
|
||||||
certs: map[string]bool{},
|
certs: map[string]bool{},
|
||||||
}
|
}
|
||||||
|
|
||||||
sftpAuthError = newAuthenticationError(nil, "", "")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type commandExecutor interface {
|
||||||
|
CombinedOutput(ctx context.Context, name string, args ...string) ([]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type defaultExecutor struct{}
|
||||||
|
|
||||||
|
func (d defaultExecutor) CombinedOutput(ctx context.Context, name string, args ...string) ([]byte, error) {
|
||||||
|
cmd := exec.CommandContext(ctx, name, args...)
|
||||||
|
cmd.Env = []string{}
|
||||||
|
return cmd.CombinedOutput()
|
||||||
|
}
|
||||||
|
|
||||||
// Binding defines the configuration for a network listener
|
// Binding defines the configuration for a network listener
|
||||||
type Binding struct {
|
type Binding struct {
|
||||||
// The address to listen on. A blank value means listen on all available network interfaces.
|
// The address to listen on. A blank value means listen on all available network interfaces.
|
||||||
@@ -150,6 +163,10 @@ type Configuration struct {
|
|||||||
// Example content:
|
// Example content:
|
||||||
// ["SHA256:bsBRHC/xgiqBJdSuvSTNpJNLTISP/G356jNMCRYC5Es","SHA256:119+8cL/HH+NLMawRsJx6CzPF1I3xC+jpM60bQHXGE8"]
|
// ["SHA256:bsBRHC/xgiqBJdSuvSTNpJNLTISP/G356jNMCRYC5Es","SHA256:119+8cL/HH+NLMawRsJx6CzPF1I3xC+jpM60bQHXGE8"]
|
||||||
RevokedUserCertsFile string `json:"revoked_user_certs_file" mapstructure:"revoked_user_certs_file"`
|
RevokedUserCertsFile string `json:"revoked_user_certs_file" mapstructure:"revoked_user_certs_file"`
|
||||||
|
// Absolute path to the opkssh binary used for OpenPubkey SSH integration
|
||||||
|
OPKSSHPath string `json:"opkssh_path" mapstructure:"opkssh_path"`
|
||||||
|
// Expected SHA256 checksum of the opkssh binary. It is verified at application startup
|
||||||
|
OPKSSHChecksum string `json:"opkssh_checksum" mapstructure:"opkssh_checksum"`
|
||||||
// LoginBannerFile the contents of the specified file, if any, are sent to
|
// LoginBannerFile the contents of the specified file, if any, are sent to
|
||||||
// the remote user before authentication is allowed.
|
// the remote user before authentication is allowed.
|
||||||
LoginBannerFile string `json:"login_banner_file" mapstructure:"login_banner_file"`
|
LoginBannerFile string `json:"login_banner_file" mapstructure:"login_banner_file"`
|
||||||
@@ -185,6 +202,7 @@ type Configuration struct {
|
|||||||
PasswordAuthentication bool `json:"password_authentication" mapstructure:"password_authentication"`
|
PasswordAuthentication bool `json:"password_authentication" mapstructure:"password_authentication"`
|
||||||
certChecker *ssh.CertChecker
|
certChecker *ssh.CertChecker
|
||||||
parsedUserCAKeys []ssh.PublicKey
|
parsedUserCAKeys []ssh.PublicKey
|
||||||
|
executor commandExecutor
|
||||||
}
|
}
|
||||||
|
|
||||||
type authenticationError struct {
|
type authenticationError struct {
|
||||||
@@ -342,6 +360,7 @@ func (c *Configuration) loadFromProvider() error {
|
|||||||
|
|
||||||
// Initialize the SFTP server and add a persistent listener to handle inbound SFTP connections.
|
// Initialize the SFTP server and add a persistent listener to handle inbound SFTP connections.
|
||||||
func (c *Configuration) Initialize(configDir string) error {
|
func (c *Configuration) Initialize(configDir string) error {
|
||||||
|
c.executor = defaultExecutor{}
|
||||||
if err := c.loadFromProvider(); err != nil {
|
if err := c.loadFromProvider(); err != nil {
|
||||||
return fmt.Errorf("unable to load configs from provider: %w", err)
|
return fmt.Errorf("unable to load configs from provider: %w", err)
|
||||||
}
|
}
|
||||||
@@ -365,6 +384,9 @@ func (c *Configuration) Initialize(configDir string) error {
|
|||||||
if err := c.initializeCertChecker(configDir); err != nil {
|
if err := c.initializeCertChecker(configDir); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := c.initializeOPKSSH(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
c.configureKeyboardInteractiveAuth(serverConfig)
|
c.configureKeyboardInteractiveAuth(serverConfig)
|
||||||
c.configureLoginBanner(serverConfig, configDir)
|
c.configureLoginBanner(serverConfig, configDir)
|
||||||
c.checkSSHCommands()
|
c.checkSSHCommands()
|
||||||
@@ -1069,6 +1091,49 @@ func (c *Configuration) loadHostCertificates(configDir string) ([]hostCertificat
|
|||||||
return certs, nil
|
return certs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Configuration) initializeOPKSSH() error {
|
||||||
|
if c.OPKSSHPath != "" {
|
||||||
|
if len(c.parsedUserCAKeys) > 0 {
|
||||||
|
return errors.New("opkssh and certificate authorities are mutually exclusive")
|
||||||
|
}
|
||||||
|
if !util.IsFileInputValid(c.OPKSSHPath) || !filepath.IsAbs(c.OPKSSHPath) {
|
||||||
|
return fmt.Errorf("opkssh path %q is not valid, it must be an absolute path", c.OPKSSHPath)
|
||||||
|
}
|
||||||
|
if c.OPKSSHChecksum == "" {
|
||||||
|
if _, err := os.Stat(c.OPKSSHPath); err != nil {
|
||||||
|
return fmt.Errorf("error validating opkssh path %q: %w", c.OPKSSHPath, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := util.VerifyFileChecksum(c.OPKSSHPath, sha256.New(), c.OPKSSHChecksum, 100*1024*1024); err != nil {
|
||||||
|
return fmt.Errorf("error validating opkssh checksum: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Configuration) verifyWithOPKSSH(username string, cert *ssh.Certificate) error {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
args := []string{"verify", username, util.BytesToString(ssh.MarshalAuthorizedKey(cert)), cert.Type()}
|
||||||
|
out, err := c.executor.CombinedOutput(ctx, c.OPKSSHPath, args...)
|
||||||
|
if err != nil {
|
||||||
|
logger.Debug(logSender, "", "unable to execute opk verifier: %s", string(out))
|
||||||
|
return fmt.Errorf("unable to execute opk verifier: %w", err)
|
||||||
|
}
|
||||||
|
pubKey, _, _, _, err := ssh.ParseAuthorizedKey(out) //nolint:dogsled
|
||||||
|
if err != nil {
|
||||||
|
logger.Debug(logSender, "", "unable to validate the opk verifier output: %s", string(out))
|
||||||
|
return fmt.Errorf("unable to validate the opk verifier output: %w", err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(pubKey.Marshal(), cert.SignatureKey.Marshal()) {
|
||||||
|
return errors.New("unable to validate opk result")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Configuration) initializeCertChecker(configDir string) error {
|
func (c *Configuration) initializeCertChecker(configDir string) error {
|
||||||
for _, keyPath := range c.TrustedUserCAKeys {
|
for _, keyPath := range c.TrustedUserCAKeys {
|
||||||
keyPath = strings.TrimSpace(keyPath)
|
keyPath = strings.TrimSpace(keyPath)
|
||||||
@@ -1144,34 +1209,43 @@ func (c *Configuration) validatePublicKeyCredentials(conn ssh.ConnMetadata, pubK
|
|||||||
var certFingerprint string
|
var certFingerprint string
|
||||||
if ok {
|
if ok {
|
||||||
certFingerprint = ssh.FingerprintSHA256(cert.Key)
|
certFingerprint = ssh.FingerprintSHA256(cert.Key)
|
||||||
if cert.CertType != ssh.UserCert {
|
if c.OPKSSHPath != "" {
|
||||||
err := fmt.Errorf("ssh: cert has type %d", cert.CertType)
|
if err := c.verifyWithOPKSSH(conn.User(), cert); err != nil {
|
||||||
user.Username = conn.User()
|
err := fmt.Errorf("ssh: verification with OPK failed: %v", err)
|
||||||
updateLoginMetrics(&user, ipAddr, method, err)
|
user.Username = conn.User()
|
||||||
return nil, err
|
updateLoginMetrics(&user, ipAddr, method, err)
|
||||||
}
|
return nil, err
|
||||||
if !c.certChecker.IsUserAuthority(cert.SignatureKey) {
|
}
|
||||||
err := errors.New("ssh: certificate signed by unrecognized authority")
|
} else {
|
||||||
user.Username = conn.User()
|
if cert.CertType != ssh.UserCert {
|
||||||
updateLoginMetrics(&user, ipAddr, method, err)
|
err := fmt.Errorf("ssh: cert has type %d", cert.CertType)
|
||||||
return nil, err
|
user.Username = conn.User()
|
||||||
}
|
updateLoginMetrics(&user, ipAddr, method, err)
|
||||||
if len(cert.ValidPrincipals) == 0 {
|
return nil, err
|
||||||
err := fmt.Errorf("ssh: certificate %s has no valid principals, user: \"%s\"", certFingerprint, conn.User())
|
}
|
||||||
user.Username = conn.User()
|
if !c.certChecker.IsUserAuthority(cert.SignatureKey) {
|
||||||
updateLoginMetrics(&user, ipAddr, method, err)
|
err := errors.New("ssh: certificate signed by unrecognized authority")
|
||||||
return nil, err
|
user.Username = conn.User()
|
||||||
}
|
updateLoginMetrics(&user, ipAddr, method, err)
|
||||||
if revokedCertManager.isRevoked(certFingerprint) {
|
return nil, err
|
||||||
err := fmt.Errorf("ssh: certificate %s is revoked", certFingerprint)
|
}
|
||||||
user.Username = conn.User()
|
if len(cert.ValidPrincipals) == 0 {
|
||||||
updateLoginMetrics(&user, ipAddr, method, err)
|
err := fmt.Errorf("ssh: certificate %s has no valid principals, user: \"%s\"", certFingerprint, conn.User())
|
||||||
return nil, err
|
user.Username = conn.User()
|
||||||
}
|
updateLoginMetrics(&user, ipAddr, method, err)
|
||||||
if err := c.certChecker.CheckCert(conn.User(), cert); err != nil {
|
return nil, err
|
||||||
user.Username = conn.User()
|
}
|
||||||
updateLoginMetrics(&user, ipAddr, method, err)
|
if revokedCertManager.isRevoked(certFingerprint) {
|
||||||
return nil, err
|
err := fmt.Errorf("ssh: certificate %s is revoked", certFingerprint)
|
||||||
|
user.Username = conn.User()
|
||||||
|
updateLoginMetrics(&user, ipAddr, method, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := c.certChecker.CheckCert(conn.User(), cert); err != nil {
|
||||||
|
user.Username = conn.User()
|
||||||
|
updateLoginMetrics(&user, ipAddr, method, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
certPerm = &cert.Permissions
|
certPerm = &cert.Permissions
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -488,6 +488,17 @@ func TestInitialization(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
sftpdConf.HostKeys = nil
|
sftpdConf.HostKeys = nil
|
||||||
sftpdConf.HostCertificates = nil
|
sftpdConf.HostCertificates = nil
|
||||||
|
sftpdConf.OPKSSHPath = "relative path"
|
||||||
|
err = sftpdConf.Initialize(configDir)
|
||||||
|
assert.Error(t, err)
|
||||||
|
sftpdConf.OPKSSHPath = filepath.Join(os.TempDir(), "missing path")
|
||||||
|
err = sftpdConf.Initialize(configDir)
|
||||||
|
assert.Error(t, err)
|
||||||
|
sftpdConf.OPKSSHChecksum = "invalid checksum"
|
||||||
|
err = sftpdConf.Initialize(configDir)
|
||||||
|
assert.Error(t, err)
|
||||||
|
sftpdConf.OPKSSHPath = ""
|
||||||
|
sftpdConf.OPKSSHChecksum = ""
|
||||||
sftpdConf.RevokedUserCertsFile = "."
|
sftpdConf.RevokedUserCertsFile = "."
|
||||||
err = sftpdConf.Initialize(configDir)
|
err = sftpdConf.Initialize(configDir)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import (
|
|||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
|
"crypto/subtle"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
@@ -30,6 +31,7 @@ import (
|
|||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"hash"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"math"
|
"math"
|
||||||
@@ -930,3 +932,40 @@ func SlicesEqual(s1, s2 []string) bool {
|
|||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// VerifyFileChecksum computes the hash of the given file using the provided
|
||||||
|
// hash algorithm and compares it against the expected checksum (in hex format).
|
||||||
|
// It returns an error if the checksum does not match or if the operation fails.
|
||||||
|
func VerifyFileChecksum(filePath string, h hash.Hash, expectedHex string, maxSize int64) error {
|
||||||
|
expected, err := hex.DecodeString(expectedHex)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid checksum %q: %w", expectedHex, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Open(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
if maxSize > 0 {
|
||||||
|
fi, err := f.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if fi.Size() > maxSize {
|
||||||
|
return fmt.Errorf("file too large: %s", ByteCountIEC(fi.Size()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := io.Copy(h, f); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
actual := h.Sum(nil)
|
||||||
|
if subtle.ConstantTimeCompare(actual, expected) != 1 {
|
||||||
|
return errors.New("checksum mismatch")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -100,6 +100,8 @@
|
|||||||
"public_key_algorithms": [],
|
"public_key_algorithms": [],
|
||||||
"trusted_user_ca_keys": [],
|
"trusted_user_ca_keys": [],
|
||||||
"revoked_user_certs_file": "",
|
"revoked_user_certs_file": "",
|
||||||
|
"opkssh_path": "",
|
||||||
|
"opkssh_checksum": "",
|
||||||
"login_banner_file": "",
|
"login_banner_file": "",
|
||||||
"enabled_ssh_commands": [
|
"enabled_ssh_commands": [
|
||||||
"md5sum",
|
"md5sum",
|
||||||
|
|||||||
Reference in New Issue
Block a user