diff --git a/cmd/tk/bootstrap_github.go b/cmd/tk/bootstrap_github.go index bf455ab1..cd3e5ac5 100644 --- a/cmd/tk/bootstrap_github.go +++ b/cmd/tk/bootstrap_github.go @@ -27,7 +27,7 @@ import ( "github.com/spf13/cobra" - "github.com/fluxcd/toolkit/pkg/git" + "github.com/fluxcd/pkg/git" ) var bootstrapGitHubCmd = &cobra.Command{ diff --git a/cmd/tk/bootstrap_gitlab.go b/cmd/tk/bootstrap_gitlab.go index 9fd3af4b..39635b96 100644 --- a/cmd/tk/bootstrap_gitlab.go +++ b/cmd/tk/bootstrap_gitlab.go @@ -27,7 +27,7 @@ import ( "github.com/spf13/cobra" - "github.com/fluxcd/toolkit/pkg/git" + "github.com/fluxcd/pkg/git" ) var bootstrapGitLabCmd = &cobra.Command{ diff --git a/cmd/tk/create_source_git.go b/cmd/tk/create_source_git.go index b7fa8fdc..78d183eb 100644 --- a/cmd/tk/create_source_git.go +++ b/cmd/tk/create_source_git.go @@ -35,7 +35,7 @@ import ( "k8s.io/apimachinery/pkg/util/wait" "sigs.k8s.io/controller-runtime/pkg/client" - "github.com/fluxcd/toolkit/pkg/ssh" + "github.com/fluxcd/pkg/ssh" ) var createSourceGitCmd = &cobra.Command{ diff --git a/go.mod b/go.mod index 150febce..c358691d 100644 --- a/go.mod +++ b/go.mod @@ -5,15 +5,12 @@ go 1.14 require ( github.com/blang/semver v3.5.1+incompatible github.com/fluxcd/kustomize-controller v0.0.1 + github.com/fluxcd/pkg v0.0.1 github.com/fluxcd/source-controller v0.0.1 - github.com/go-git/go-git/v5 v5.0.0 github.com/golang/protobuf v1.4.2 // indirect - github.com/google/go-github/v32 v32.0.0 github.com/hashicorp/go-retryablehttp v0.6.6 // indirect github.com/manifoldco/promptui v0.7.0 github.com/spf13/cobra v1.0.0 - github.com/xanzy/go-gitlab v0.32.1 - golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 golang.org/x/net v0.0.0-20200602114024-627f9648deb9 // indirect golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d // indirect golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 // indirect diff --git a/go.sum b/go.sum index 2056a267..20f5fb89 100644 --- a/go.sum +++ b/go.sum @@ -170,6 +170,8 @@ github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwo github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fluxcd/kustomize-controller v0.0.1 h1:F2wg9c5nMUEnPHgs44HMY1/2UAXXaYcmpj7WeOzf9p0= github.com/fluxcd/kustomize-controller v0.0.1/go.mod h1:sSIy+Y924OGHW2anzZvD53BbgjSOO4mONTTG2+UTEhM= +github.com/fluxcd/pkg v0.0.1 h1:yECp5SBjX7vUBOjd3KYBoVQwt22A0u1SZJjYV4PduAk= +github.com/fluxcd/pkg v0.0.1/go.mod h1:3DgEcVmkVYrA/BDb/fyDIJllxK++c/ovLCMPRlkAp9Y= github.com/fluxcd/source-controller v0.0.1 h1:17/b/Zcb3OUkUoo03W+L7TGwkCKG23K9HrgL+d5WMXE= github.com/fluxcd/source-controller v0.0.1/go.mod h1:tmscNdCxEt7+Xt2g1+bI38hMPw2leYMFAaCn4UlMGuw= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ= @@ -195,6 +197,8 @@ github.com/go-git/go-git-fixtures/v4 v4.0.1 h1:q+IFMfLx200Q3scvt2hN79JsEzy4AmBTp github.com/go-git/go-git-fixtures/v4 v4.0.1/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw= github.com/go-git/go-git/v5 v5.0.0 h1:k5RWPm4iJwYtfWoxIJy4wJX9ON7ihPeZZYC1fLYDnpg= github.com/go-git/go-git/v5 v5.0.0/go.mod h1:oYD8y9kWsGINPFJoLdaScGCN6dlKg23blmClfZwtUVA= +github.com/go-git/go-git/v5 v5.1.0 h1:HxJn9g/E7eYvKW3Fm7Jt4ee8LXfPOm/H1cdDu8vEssk= +github.com/go-git/go-git/v5 v5.1.0/go.mod h1:ZKfuPUoY1ZqIG4QG9BDBh3G4gLM5zvPuSJAozQrZuyM= github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-lintpack/lintpack v0.5.2/go.mod h1:NwZuYi2nUHho8XEIZ6SIxihrnPoqBTDqfpXvXAN0sXM= @@ -394,6 +398,8 @@ github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJ github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI= github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.9 h1:UauaLniWCFHWd+Jp9oCEkTBj8VO/9DKg3PV3VCNMDIg= +github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= @@ -688,6 +694,8 @@ golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975 h1:/Tl7pH94bvbAAHBdZJT947 golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM= golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190312203227-4b39c73a6495/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= diff --git a/pkg/git/provider.go b/pkg/git/provider.go deleted file mode 100644 index 902a0702..00000000 --- a/pkg/git/provider.go +++ /dev/null @@ -1,26 +0,0 @@ -/* -Copyright 2020 The Flux CD contributors. - -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. -*/ - -package git - -import "context" - -// Provider is the interface that a git provider should implement -type Provider interface { - CreateRepository(ctx context.Context, r *Repository) (bool, error) - AddTeam(ctx context.Context, r *Repository, name, permission string) (bool, error) - AddDeployKey(ctx context.Context, r *Repository, key, keyName string) (bool, error) -} diff --git a/pkg/git/provider_github.go b/pkg/git/provider_github.go deleted file mode 100644 index a2340478..00000000 --- a/pkg/git/provider_github.go +++ /dev/null @@ -1,177 +0,0 @@ -/* -Copyright 2020 The Flux CD contributors. - -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. -*/ - -package git - -import ( - "context" - "fmt" - "github.com/google/go-github/v32/github" - "strings" -) - -// GithubProvider represents a GitHub API wrapper -type GithubProvider struct { - IsPrivate bool - IsPersonal bool -} - -const ( - GitHubTokenName = "GITHUB_TOKEN" - GitHubDefaultHostname = "github.com" -) - -func (p *GithubProvider) newClient(r *Repository) (*github.Client, error) { - auth := github.BasicAuthTransport{ - Username: "git", - Password: r.Token, - } - - gh := github.NewClient(auth.Client()) - if r.Host != GitHubDefaultHostname { - baseURL := fmt.Sprintf("https://%s/api/v3/", r.Host) - uploadURL := fmt.Sprintf("https://%s/api/uploads/", r.Host) - if g, err := github.NewEnterpriseClient(baseURL, uploadURL, auth.Client()); err == nil { - gh = g - } else { - return nil, err - } - } - - return gh, nil -} - -// CreateRepository returns false if the repository exists -func (p *GithubProvider) CreateRepository(ctx context.Context, r *Repository) (bool, error) { - gh, err := p.newClient(r) - if err != nil { - return false, fmt.Errorf("client error: %w", err) - } - org := "" - if !p.IsPersonal { - org = r.Owner - } - - if _, _, err := gh.Repositories.Get(ctx, org, r.Name); err == nil { - return false, nil - } - - autoInit := true - _, _, err = gh.Repositories.Create(ctx, org, &github.Repository{ - AutoInit: &autoInit, - Name: &r.Name, - Private: &p.IsPrivate, - }) - if err != nil { - if !strings.Contains(err.Error(), "name already exists on this account") { - return false, fmt.Errorf("failed to create repository, error: %w", err) - } - } else { - return true, nil - } - return false, nil -} - -// AddTeam returns false if the team is already assigned to the repository -func (p *GithubProvider) AddTeam(ctx context.Context, r *Repository, name, permission string) (bool, error) { - gh, err := p.newClient(r) - if err != nil { - return false, fmt.Errorf("client error: %w", err) - } - - // check team exists - _, _, err = gh.Teams.GetTeamBySlug(ctx, r.Owner, name) - if err != nil { - return false, fmt.Errorf("failed to retrieve team '%s', error: %w", name, err) - } - - // check if team is assigned to the repo - _, resp, err := gh.Teams.IsTeamRepoBySlug(ctx, r.Owner, name, r.Owner, r.Name) - if resp == nil && err != nil { - return false, fmt.Errorf("failed to determine if team '%s' is assigned to the repository, error: %w", name, err) - } - - // add team to the repo - if resp.StatusCode == 404 { - _, err = gh.Teams.AddTeamRepoBySlug(ctx, r.Owner, name, r.Owner, r.Name, &github.TeamAddTeamRepoOptions{ - Permission: permission, - }) - if err != nil { - return false, fmt.Errorf("failed to add team '%s' to the repository, error: %w", name, err) - } - return true, nil - } - - return false, nil -} - -// AddDeployKey returns false if the key exists and the content is the same -func (p *GithubProvider) AddDeployKey(ctx context.Context, r *Repository, key, keyName string) (bool, error) { - gh, err := p.newClient(r) - if err != nil { - return false, fmt.Errorf("client error: %w", err) - } - - // list deploy keys - keys, resp, err := gh.Repositories.ListKeys(ctx, r.Owner, r.Name, nil) - if err != nil { - return false, fmt.Errorf("failed to list deploy keys, error: %w", err) - } - if resp.StatusCode >= 300 { - return false, fmt.Errorf("failed to list deploy keys (status code: %s)", resp.Status) - } - - // check if the key exists - shouldCreateKey := true - var existingKey *github.Key - for _, k := range keys { - if k.Title != nil && k.Key != nil && *k.Title == keyName { - if *k.Key != key { - existingKey = k - } else { - shouldCreateKey = false - } - break - } - } - - // delete existing key if the value differs - if existingKey != nil { - resp, err := gh.Repositories.DeleteKey(ctx, r.Owner, r.Name, *existingKey.ID) - if err != nil { - return false, fmt.Errorf("failed to delete deploy key '%s', error: %w", keyName, err) - } - if resp.StatusCode >= 300 { - return false, fmt.Errorf("failed to delete deploy key '%s' (status code: %s)", keyName, resp.Status) - } - } - - // create key - if shouldCreateKey { - isReadOnly := true - _, _, err = gh.Repositories.CreateKey(ctx, r.Owner, r.Name, &github.Key{ - Title: &keyName, - Key: &key, - ReadOnly: &isReadOnly, - }) - if err != nil { - return false, fmt.Errorf("failed to create deploy key '%s', error: %w", keyName, err) - } - return true, nil - } - - return false, nil -} diff --git a/pkg/git/provider_gitlab.go b/pkg/git/provider_gitlab.go deleted file mode 100644 index c3012435..00000000 --- a/pkg/git/provider_gitlab.go +++ /dev/null @@ -1,163 +0,0 @@ -/* -Copyright 2020 The Flux CD contributors. - -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. -*/ - -package git - -import ( - "context" - "fmt" - "github.com/xanzy/go-gitlab" -) - -// GitLabProvider represents a GitLab API wrapper -type GitLabProvider struct { - IsPrivate bool - IsPersonal bool -} - -const ( - GitLabTokenName = "GITLAB_TOKEN" - GitLabDefaultHostname = "gitlab.com" -) - -func (p *GitLabProvider) newClient(r *Repository) (*gitlab.Client, error) { - gl, err := gitlab.NewClient(r.Token) - if err != nil { - return nil, err - } - - if r.Host != GitLabDefaultHostname { - gl, err = gitlab.NewClient(r.Token, gitlab.WithBaseURL(fmt.Sprintf("https://%s/api/v4", r.Host))) - if err != nil { - return nil, err - } - } - return gl, nil -} - -// CreateRepository returns false if the repository already exists -func (p *GitLabProvider) CreateRepository(ctx context.Context, r *Repository) (bool, error) { - gl, err := p.newClient(r) - if err != nil { - return false, fmt.Errorf("client error: %w", err) - } - - var id *int - if !p.IsPersonal { - groups, _, err := gl.Groups.ListGroups(&gitlab.ListGroupsOptions{Search: gitlab.String(r.Owner)}, gitlab.WithContext(ctx)) - if err != nil { - return false, fmt.Errorf("failed to list groups, error: %w", err) - } - - if len(groups) > 0 { - id = &groups[0].ID - } - } - - visibility := gitlab.PublicVisibility - if p.IsPrivate { - visibility = gitlab.PrivateVisibility - } - - projects, _, err := gl.Projects.ListProjects(&gitlab.ListProjectsOptions{Search: gitlab.String(r.Name)}, gitlab.WithContext(ctx)) - if err != nil { - return false, fmt.Errorf("failed to list projects, error: %w", err) - } - - if len(projects) == 0 { - p := &gitlab.CreateProjectOptions{ - Name: gitlab.String(r.Name), - NamespaceID: id, - Visibility: &visibility, - InitializeWithReadme: gitlab.Bool(true), - } - - _, _, err := gl.Projects.CreateProject(p) - if err != nil { - return false, fmt.Errorf("failed to create project, error: %w", err) - } - return true, nil - } - - return false, nil -} - -// AddTeam returns false if the team is already assigned to the repository -func (p *GitLabProvider) AddTeam(ctx context.Context, r *Repository, name, permission string) (bool, error) { - return false, nil -} - -// AddDeployKey returns false if the key exists and the content is the same -func (p *GitLabProvider) AddDeployKey(ctx context.Context, r *Repository, key, keyName string) (bool, error) { - gl, err := p.newClient(r) - if err != nil { - return false, fmt.Errorf("client error: %w", err) - } - - // list deploy keys - var projId int - projects, _, err := gl.Projects.ListProjects(&gitlab.ListProjectsOptions{Search: gitlab.String(r.Name)}, gitlab.WithContext(ctx)) - if err != nil { - return false, fmt.Errorf("failed to list projects, error: %w", err) - } - if len(projects) > 0 { - projId = projects[0].ID - } else { - return false, fmt.Errorf("no project found") - } - - // check if the key exists - keys, _, err := gl.DeployKeys.ListProjectDeployKeys(projId, &gitlab.ListProjectDeployKeysOptions{}) - if err != nil { - return false, fmt.Errorf("failed to list deploy keys, error: %w", err) - } - - shouldCreateKey := true - var existingKey *gitlab.DeployKey - for _, k := range keys { - if k.Title == keyName { - if k.Key != key { - existingKey = k - } else { - shouldCreateKey = false - } - break - } - } - - // delete existing key if the value differs - if existingKey != nil { - _, err := gl.DeployKeys.DeleteDeployKey(projId, existingKey.ID, gitlab.WithContext(ctx)) - if err != nil { - return false, fmt.Errorf("failed to delete deploy key '%s', error: %w", keyName, err) - } - } - - // create key - if shouldCreateKey { - _, _, err := gl.DeployKeys.AddDeployKey(projId, &gitlab.AddDeployKeyOptions{ - Title: gitlab.String(keyName), - Key: gitlab.String(key), - CanPush: gitlab.Bool(false), - }, gitlab.WithContext(ctx)) - if err != nil { - return false, fmt.Errorf("failed to create deploy key '%s', error: %w", keyName, err) - } - return true, nil - } - - return false, nil -} diff --git a/pkg/git/repository.go b/pkg/git/repository.go deleted file mode 100644 index ab029063..00000000 --- a/pkg/git/repository.go +++ /dev/null @@ -1,167 +0,0 @@ -/* -Copyright 2020 The Flux CD contributors. - -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. -*/ - -package git - -import ( - "context" - "fmt" - "time" - - "github.com/go-git/go-git/v5" - "github.com/go-git/go-git/v5/plumbing" - "github.com/go-git/go-git/v5/plumbing/object" - "github.com/go-git/go-git/v5/plumbing/transport" - "github.com/go-git/go-git/v5/plumbing/transport/http" -) - -// Repository represents a git repository wrapper -type Repository struct { - Name string - Owner string - Host string - Token string - AuthorName string - AuthorEmail string - - repo *git.Repository -} - -// NewRepository returns a git repository wrapper -func NewRepository(name, owner, host, token, authorName, authorEmail string) (*Repository, error) { - if name == "" { - return nil, fmt.Errorf("name required") - } - if owner == "" { - return nil, fmt.Errorf("owner required") - } - if host == "" { - return nil, fmt.Errorf("host required") - } - if token == "" { - return nil, fmt.Errorf("token required") - } - if authorName == "" { - return nil, fmt.Errorf("author name required") - } - if authorEmail == "" { - return nil, fmt.Errorf("author email required") - } - - return &Repository{ - Name: name, - Owner: owner, - Host: host, - Token: token, - AuthorName: authorName, - AuthorEmail: authorEmail, - }, nil -} - -// GetURL returns the repository HTTPS address -func (r *Repository) GetURL() string { - return fmt.Sprintf("https://%s/%s/%s", r.Host, r.Owner, r.Name) -} - -// GetSSH returns the repository SSH address -func (r *Repository) GetSSH() string { - return fmt.Sprintf("ssh://git@%s/%s/%s", r.Host, r.Owner, r.Name) -} - -func (r *Repository) auth() transport.AuthMethod { - return &http.BasicAuth{ - Username: "git", - Password: r.Token, - } -} - -// Checkout repository branch at specified path -func (r *Repository) Checkout(ctx context.Context, branch, path string) error { - repo, err := git.PlainCloneContext(ctx, path, false, &git.CloneOptions{ - URL: r.GetURL(), - Auth: r.auth(), - RemoteName: git.DefaultRemoteName, - ReferenceName: plumbing.NewBranchReferenceName(branch), - SingleBranch: true, - NoCheckout: false, - Progress: nil, - Tags: git.NoTags, - }) - if err != nil { - return fmt.Errorf("git clone error: %w", err) - } - - _, err = repo.Head() - if err != nil { - return fmt.Errorf("git resolve HEAD error: %w", err) - } - - r.repo = repo - return nil -} - -// Commit changes for the specified path, returns false if no changes are detected -func (r *Repository) Commit(ctx context.Context, path, message string) (bool, error) { - if r.repo == nil { - return false, fmt.Errorf("repository hasn't been cloned") - } - - w, err := r.repo.Worktree() - if err != nil { - return false, err - } - - _, err = w.Add(path) - if err != nil { - return false, err - } - - status, err := w.Status() - if err != nil { - return false, err - } - - if !status.IsClean() { - if _, err := w.Commit(message, &git.CommitOptions{ - Author: &object.Signature{ - Name: r.AuthorName, - Email: r.AuthorEmail, - When: time.Now(), - }, - }); err != nil { - return false, err - } - return true, nil - } - - return false, nil -} - -// Push commits to origin -func (r *Repository) Push(ctx context.Context) error { - if r.repo == nil { - return fmt.Errorf("repository hasn't been cloned") - } - - err := r.repo.PushContext(ctx, &git.PushOptions{ - Auth: r.auth(), - Progress: nil, - }) - if err != nil { - return fmt.Errorf("git push error: %w", err) - } - return nil -} diff --git a/pkg/ssh/host_key.go b/pkg/ssh/host_key.go deleted file mode 100644 index 52bec8dc..00000000 --- a/pkg/ssh/host_key.go +++ /dev/null @@ -1,71 +0,0 @@ -/* -Copyright 2020 The Flux CD contributors. - -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. -*/ - -package ssh - -import ( - "encoding/base64" - "fmt" - "net" - "time" - - "golang.org/x/crypto/ssh" - "golang.org/x/crypto/ssh/knownhosts" -) - -// ScanHostKey collects the given host's preferred public key for the -// Any errors (e.g. authentication failures) are ignored, except if -// no key could be collected from the host. -func ScanHostKey(host string, timeout time.Duration) ([]byte, error) { - col := &HostKeyCollector{} - config := &ssh.ClientConfig{ - HostKeyCallback: col.StoreKey(), - Timeout: timeout, - } - client, err := ssh.Dial("tcp", host, config) - if err == nil { - defer client.Close() - } - if len(col.knownKeys) > 0 { - return col.knownKeys, nil - } - return col.knownKeys, err -} - -// HostKeyCollector offers a StoreKey method which provides an -// HostKeyCallBack to collect public keys from an SSH server. -type HostKeyCollector struct { - knownKeys []byte -} - -// StoreKey stores the public key in bytes as returned by the host. -// To collect multiple public key types from the host, multiple -// SSH dials need with the ClientConfig HostKeyAlgorithms set to -// the algorithm you want to collect. -func (c *HostKeyCollector) StoreKey() ssh.HostKeyCallback { - return func(hostname string, remote net.Addr, key ssh.PublicKey) error { - c.knownKeys = append( - c.knownKeys, - fmt.Sprintf("%s %s %s\n", knownhosts.Normalize(hostname), key.Type(), base64.StdEncoding.EncodeToString(key.Marshal()))..., - ) - return nil - } -} - -// GetKnownKeys returns the collected public keys in bytes. -func (c *HostKeyCollector) GetKnownKeys() []byte { - return c.knownKeys -} diff --git a/pkg/ssh/key_pair.go b/pkg/ssh/key_pair.go deleted file mode 100644 index 3a27fdfd..00000000 --- a/pkg/ssh/key_pair.go +++ /dev/null @@ -1,146 +0,0 @@ -/* -Copyright 2020 The Flux CD contributors. - -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. -*/ - -package ssh - -import ( - "crypto/ecdsa" - "crypto/ed25519" - "crypto/elliptic" - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "encoding/pem" - - "golang.org/x/crypto/ssh" -) - -// KeyPair holds the public and private key PEM block bytes. -type KeyPair struct { - PublicKey []byte - PrivateKey []byte -} - -type KeyPairGenerator interface { - Generate() (*KeyPair, error) -} - -type RSAGenerator struct { - bits int -} - -func NewRSAGenerator(bits int) KeyPairGenerator { - return &RSAGenerator{bits} -} - -func (g *RSAGenerator) Generate() (*KeyPair, error) { - pk, err := rsa.GenerateKey(rand.Reader, g.bits) - if err != nil { - return nil, err - } - err = pk.Validate() - if err != nil { - return nil, err - } - pub, err := generatePublicKey(&pk.PublicKey) - if err != nil { - return nil, err - } - priv, err := encodePrivateKeyToPEM(pk) - if err != nil { - return nil, err - } - return &KeyPair{ - PublicKey: pub, - PrivateKey: priv, - }, nil -} - -type ECDSAGenerator struct { - c elliptic.Curve -} - -func NewECDSAGenerator(c elliptic.Curve) KeyPairGenerator { - return &ECDSAGenerator{c} -} - -func (g *ECDSAGenerator) Generate() (*KeyPair, error) { - pk, err := ecdsa.GenerateKey(g.c, rand.Reader) - if err != nil { - return nil, err - } - pub, err := generatePublicKey(&pk.PublicKey) - if err != nil { - return nil, err - } - priv, err := encodePrivateKeyToPEM(pk) - if err != nil { - return nil, err - } - return &KeyPair{ - PublicKey: pub, - PrivateKey: priv, - }, nil -} - -type Ed25519Generator struct{} - -func NewEd25519Generator() KeyPairGenerator { - return &Ed25519Generator{} -} - -func (g *Ed25519Generator) Generate() (*KeyPair, error) { - pk, pv, err := ed25519.GenerateKey(rand.Reader) - if err != nil { - return nil, err - } - pub, err := generatePublicKey(pk) - if err != nil { - return nil, err - } - priv, err := encodePrivateKeyToPEM(pv) - if err != nil { - return nil, err - } - return &KeyPair{ - PublicKey: pub, - PrivateKey: priv, - }, nil -} - -func generatePublicKey(pk interface{}) ([]byte, error) { - b, err := ssh.NewPublicKey(pk) - if err != nil { - return nil, err - } - k := ssh.MarshalAuthorizedKey(b) - return k, nil -} - -// encodePrivateKeyToPEM encodes the given private key to a PEM block. -// The encoded format is PKCS#8 for universal support of the most -// common key types (rsa, ecdsa, ed25519). -func encodePrivateKeyToPEM(pk interface{}) ([]byte, error) { - b, err := x509.MarshalPKCS8PrivateKey(pk) - if err != nil { - return nil, err - } - block := pem.Block{ - Type: "PRIVATE KEY", - Bytes: b, - } - return pem.EncodeToMemory(&block), nil -} diff --git a/pkg/ssh/knownhosts/LICENSE b/pkg/ssh/knownhosts/LICENSE deleted file mode 100644 index 6a66aea5..00000000 --- a/pkg/ssh/knownhosts/LICENSE +++ /dev/null @@ -1,27 +0,0 @@ -Copyright (c) 2009 The Go Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/pkg/ssh/knownhosts/knownhosts.go b/pkg/ssh/knownhosts/knownhosts.go deleted file mode 100644 index b83d11fd..00000000 --- a/pkg/ssh/knownhosts/knownhosts.go +++ /dev/null @@ -1,446 +0,0 @@ -// Copyright 2017 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Copyright 2020 The FluxCD contributors. All rights reserved. -// This package provides an in-memory known hosts database -// derived from the golang.org/x/crypto/ssh/knownhosts -// package. -// It has been slightly modified and adapted to work with -// in-memory host keys not related to any known_hosts files -// on disk, and the database can be initialized with just a -// known_hosts byte blob. -// https://pkg.go.dev/golang.org/x/crypto/ssh/knownhosts - -package knownhosts - -import ( - "bufio" - "bytes" - "crypto/hmac" - "crypto/sha1" - "encoding/base64" - "errors" - "fmt" - "io" - "net" - "strings" - - "golang.org/x/crypto/ssh" - "golang.org/x/crypto/ssh/knownhosts" -) - -// See the sshd manpage -// (http://man.openbsd.org/sshd#SSH_KNOWN_HOSTS_FILE_FORMAT) for -// background. - -type addr struct{ host, port string } - -func (a *addr) String() string { - h := a.host - if strings.Contains(h, ":") { - h = "[" + h + "]" - } - return h + ":" + a.port -} - -type matcher interface { - match(addr) bool -} - -type hostPattern struct { - negate bool - addr addr -} - -func (p *hostPattern) String() string { - n := "" - if p.negate { - n = "!" - } - - return n + p.addr.String() -} - -type hostPatterns []hostPattern - -func (ps hostPatterns) match(a addr) bool { - matched := false - for _, p := range ps { - if !p.match(a) { - continue - } - if p.negate { - return false - } - matched = true - } - return matched -} - -// See -// https://android.googlesource.com/platform/external/openssh/+/ab28f5495c85297e7a597c1ba62e996416da7c7e/addrmatch.c -// The matching of * has no regard for separators, unlike filesystem globs -func wildcardMatch(pat []byte, str []byte) bool { - for { - if len(pat) == 0 { - return len(str) == 0 - } - if len(str) == 0 { - return false - } - - if pat[0] == '*' { - if len(pat) == 1 { - return true - } - - for j := range str { - if wildcardMatch(pat[1:], str[j:]) { - return true - } - } - return false - } - - if pat[0] == '?' || pat[0] == str[0] { - pat = pat[1:] - str = str[1:] - } else { - return false - } - } -} - -func (p *hostPattern) match(a addr) bool { - return wildcardMatch([]byte(p.addr.host), []byte(a.host)) && p.addr.port == a.port -} - -type inMemoryHostKeyDB struct { - hostKeys []hostKey - revoked map[string]*ssh.PublicKey -} - -func newInMemoryHostKeyDB() *inMemoryHostKeyDB { - db := &inMemoryHostKeyDB{ - revoked: make(map[string]*ssh.PublicKey), - } - - return db -} - -func keyEq(a, b ssh.PublicKey) bool { - return bytes.Equal(a.Marshal(), b.Marshal()) -} - -type hostKey struct { - matcher matcher - cert bool - key ssh.PublicKey -} - -func (l *hostKey) match(a addr) bool { - return l.matcher.match(a) -} - -// IsAuthorityForHost can be used as a callback in ssh.CertChecker -func (db *inMemoryHostKeyDB) IsHostAuthority(remote ssh.PublicKey, address string) bool { - h, p, err := net.SplitHostPort(address) - if err != nil { - return false - } - a := addr{host: h, port: p} - - for _, l := range db.hostKeys { - if l.cert && keyEq(l.key, remote) && l.match(a) { - return true - } - } - return false -} - -// IsRevoked can be used as a callback in ssh.CertChecker -func (db *inMemoryHostKeyDB) IsRevoked(key *ssh.Certificate) bool { - _, ok := db.revoked[string(key.Marshal())] - return ok -} - -const markerCert = "@cert-authority" -const markerRevoked = "@revoked" - -func nextWord(line []byte) (string, []byte) { - i := bytes.IndexAny(line, "\t ") - if i == -1 { - return string(line), nil - } - - return string(line[:i]), bytes.TrimSpace(line[i:]) -} - -func parseLine(line []byte) (marker, host string, key ssh.PublicKey, err error) { - if w, next := nextWord(line); w == markerCert || w == markerRevoked { - marker = w - line = next - } - - host, line = nextWord(line) - if len(line) == 0 { - return "", "", nil, errors.New("knownhosts: missing host pattern") - } - - // ignore the keytype as it's in the key blob anyway. - _, line = nextWord(line) - if len(line) == 0 { - return "", "", nil, errors.New("knownhosts: missing key type pattern") - } - - keyBlob, _ := nextWord(line) - - keyBytes, err := base64.StdEncoding.DecodeString(keyBlob) - if err != nil { - return "", "", nil, err - } - key, err = ssh.ParsePublicKey(keyBytes) - if err != nil { - return "", "", nil, err - } - - return marker, host, key, nil -} - -func (db *inMemoryHostKeyDB) parseLine(line []byte) error { - marker, pattern, key, err := parseLine(line) - if err != nil { - return err - } - - if marker == markerRevoked { - db.revoked[string(key.Marshal())] = &key - return nil - } - - entry := hostKey{ - key: key, - cert: marker == markerCert, - } - - if pattern[0] == '|' { - entry.matcher, err = newHashedHost(pattern) - } else { - entry.matcher, err = newHostnameMatcher(pattern) - } - - if err != nil { - return err - } - - db.hostKeys = append(db.hostKeys, entry) - return nil -} - -func newHostnameMatcher(pattern string) (matcher, error) { - var hps hostPatterns - for _, p := range strings.Split(pattern, ",") { - if len(p) == 0 { - continue - } - - var a addr - var negate bool - if p[0] == '!' { - negate = true - p = p[1:] - } - - if len(p) == 0 { - return nil, errors.New("knownhosts: negation without following hostname") - } - - var err error - if p[0] == '[' { - a.host, a.port, err = net.SplitHostPort(p) - if err != nil { - return nil, err - } - } else { - a.host, a.port, err = net.SplitHostPort(p) - if err != nil { - a.host = p - a.port = "22" - } - } - hps = append(hps, hostPattern{ - negate: negate, - addr: a, - }) - } - return hps, nil -} - -// check checks a key against the host database. This should not be -// used for verifying certificates. -func (db *inMemoryHostKeyDB) check(address string, remote net.Addr, remoteKey ssh.PublicKey) error { - if revoked := db.revoked[string(remoteKey.Marshal())]; revoked != nil { - return &knownhosts.RevokedError{Revoked: knownhosts.KnownKey{Key: *revoked}} - } - - host, port, err := net.SplitHostPort(remote.String()) - if err != nil { - return fmt.Errorf("knownhosts: SplitHostPort(%s): %v", remote, err) - } - - hostToCheck := addr{host, port} - if address != "" { - // Give preference to the hostname if available. - host, port, err := net.SplitHostPort(address) - if err != nil { - return fmt.Errorf("knownhosts: SplitHostPort(%s): %v", address, err) - } - - hostToCheck = addr{host, port} - } - - return db.checkAddr(hostToCheck, remoteKey) -} - -// checkAddr checks if we can find the given public key for the -// given address. If we only find an entry for the IP address, -// or only the hostname, then this still succeeds. -func (db *inMemoryHostKeyDB) checkAddr(a addr, remoteKey ssh.PublicKey) error { - // TODO(hanwen): are these the right semantics? What if there - // is just a key for the IP address, but not for the - // hostname? - - // Algorithm => key. - knownKeys := map[string]ssh.PublicKey{} - for _, l := range db.hostKeys { - if l.match(a) { - typ := l.key.Type() - if _, ok := knownKeys[typ]; !ok { - knownKeys[typ] = l.key - } - } - } - - keyErr := &knownhosts.KeyError{} - for _, v := range knownKeys { - keyErr.Want = append(keyErr.Want, knownhosts.KnownKey{Key: v}) - } - - // Unknown remote host. - if len(knownKeys) == 0 { - return keyErr - } - - // If the remote host starts using a different, unknown key type, we - // also interpret that as a mismatch. - if known, ok := knownKeys[remoteKey.Type()]; !ok || !keyEq(known, remoteKey) { - return keyErr - } - - return nil -} - -// The Read function parses file contents. -func (db *inMemoryHostKeyDB) Read(r io.Reader) error { - scanner := bufio.NewScanner(r) - - lineNum := 0 - for scanner.Scan() { - lineNum++ - line := scanner.Bytes() - line = bytes.TrimSpace(line) - if len(line) == 0 || line[0] == '#' { - continue - } - - if err := db.parseLine(line); err != nil { - return fmt.Errorf("knownhosts: %v", err) - } - } - return scanner.Err() -} - -// New creates a host key callback from the given OpenSSH host key -// file bytes. The returned callback is for use in -// ssh.ClientConfig.HostKeyCallback. By preference, the key check -// operates on the hostname if available, i.e. if a server changes its -// IP address, the host key check will still succeed, even though a -// record of the new IP address is not available. -func New(b []byte) (ssh.HostKeyCallback, error) { - db := newInMemoryHostKeyDB() - r := bytes.NewReader(b) - if err := db.Read(r); err != nil { - return nil, err - } - - var certChecker ssh.CertChecker - certChecker.IsHostAuthority = db.IsHostAuthority - certChecker.IsRevoked = db.IsRevoked - certChecker.HostKeyFallback = db.check - - return certChecker.CheckHostKey, nil -} - -func decodeHash(encoded string) (hashType string, salt, hash []byte, err error) { - if len(encoded) == 0 || encoded[0] != '|' { - err = errors.New("knownhosts: hashed host must start with '|'") - return - } - components := strings.Split(encoded, "|") - if len(components) != 4 { - err = fmt.Errorf("knownhosts: got %d components, want 3", len(components)) - return - } - - hashType = components[1] - if salt, err = base64.StdEncoding.DecodeString(components[2]); err != nil { - return - } - if hash, err = base64.StdEncoding.DecodeString(components[3]); err != nil { - return - } - return -} - -func encodeHash(typ string, salt []byte, hash []byte) string { - return strings.Join([]string{"", - typ, - base64.StdEncoding.EncodeToString(salt), - base64.StdEncoding.EncodeToString(hash), - }, "|") -} - -// See https://android.googlesource.com/platform/external/openssh/+/ab28f5495c85297e7a597c1ba62e996416da7c7e/hostfile.c#120 -func hashHost(hostname string, salt []byte) []byte { - mac := hmac.New(sha1.New, salt) - mac.Write([]byte(hostname)) - return mac.Sum(nil) -} - -type hashedHost struct { - salt []byte - hash []byte -} - -const sha1HashType = "1" - -func newHashedHost(encoded string) (*hashedHost, error) { - typ, salt, hash, err := decodeHash(encoded) - if err != nil { - return nil, err - } - - // The type field seems for future algorithm agility, but it's - // actually hardcoded in openssh currently, see - // https://android.googlesource.com/platform/external/openssh/+/ab28f5495c85297e7a597c1ba62e996416da7c7e/hostfile.c#120 - if typ != sha1HashType { - return nil, fmt.Errorf("knownhosts: got hash type %s, must be '1'", typ) - } - - return &hashedHost{salt: salt, hash: hash}, nil -} - -func (h *hashedHost) match(a addr) bool { - return bytes.Equal(hashHost(knownhosts.Normalize(a.String()), h.salt), h.hash) -} diff --git a/pkg/ssh/knownhosts/knownhosts_test.go b/pkg/ssh/knownhosts/knownhosts_test.go deleted file mode 100644 index 8d057c5a..00000000 --- a/pkg/ssh/knownhosts/knownhosts_test.go +++ /dev/null @@ -1,327 +0,0 @@ -// Copyright 2017 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Copyright 2020 The FluxCD contributors. All rights reserved. -// This package provides an in-memory known hosts database -// derived from the golang.org/x/crypto/ssh/knownhosts -// package. -// It has been slightly modified and adapted to work with -// in-memory host keys not related to any known_hosts files -// on disk, and the database can be initialized with just a -// known_hosts byte blob. -// https://pkg.go.dev/golang.org/x/crypto/ssh/knownhosts - -package knownhosts - -import ( - "bytes" - "fmt" - "net" - "reflect" - "testing" - - "golang.org/x/crypto/ssh" - "golang.org/x/crypto/ssh/knownhosts" -) - -const edKeyStr = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGBAarftlLeoyf+v+nVchEZII/vna2PCV8FaX4vsF5BX" -const alternateEdKeyStr = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIXffBYeYL+WVzVru8npl5JHt2cjlr4ornFTWzoij9sx" -const ecKeyStr = "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNLCu01+wpXe3xB5olXCN4SqU2rQu0qjSRKJO4Bg+JRCPU+ENcgdA5srTU8xYDz/GEa4dzK5ldPw4J/gZgSXCMs=" - -var ecKey, alternateEdKey, edKey ssh.PublicKey -var testAddr = &net.TCPAddr{ - IP: net.IP{198, 41, 30, 196}, - Port: 22, -} - -var testAddr6 = &net.TCPAddr{ - IP: net.IP{198, 41, 30, 196, - 1, 2, 3, 4, - 1, 2, 3, 4, - 1, 2, 3, 4, - }, - Port: 22, -} - -func init() { - var err error - ecKey, _, _, _, err = ssh.ParseAuthorizedKey([]byte(ecKeyStr)) - if err != nil { - panic(err) - } - edKey, _, _, _, err = ssh.ParseAuthorizedKey([]byte(edKeyStr)) - if err != nil { - panic(err) - } - alternateEdKey, _, _, _, err = ssh.ParseAuthorizedKey([]byte(alternateEdKeyStr)) - if err != nil { - panic(err) - } -} - -func testDB(t *testing.T, s string) *inMemoryHostKeyDB { - db := newInMemoryHostKeyDB() - if err := db.Read(bytes.NewBufferString(s)); err != nil { - t.Fatalf("Read: %v", err) - } - - return db -} - -func TestRevoked(t *testing.T) { - db := testDB(t, "\n\n@revoked * "+edKeyStr+"\n") - want := &knownhosts.RevokedError{ - Revoked: knownhosts.KnownKey{ - Key: edKey, - }, - } - if err := db.check("", &net.TCPAddr{ - Port: 42, - }, edKey); err == nil { - t.Fatal("no error for revoked key") - } else if !reflect.DeepEqual(want, err) { - t.Fatalf("got %#v, want %#v", want, err) - } -} - -func TestHostAuthority(t *testing.T) { - for _, m := range []struct { - authorityFor string - address string - - good bool - }{ - {authorityFor: "localhost", address: "localhost:22", good: true}, - {authorityFor: "localhost", address: "localhost", good: false}, - {authorityFor: "localhost", address: "localhost:1234", good: false}, - {authorityFor: "[localhost]:1234", address: "localhost:1234", good: true}, - {authorityFor: "[localhost]:1234", address: "localhost:22", good: false}, - {authorityFor: "[localhost]:1234", address: "localhost", good: false}, - } { - db := testDB(t, `@cert-authority `+m.authorityFor+` `+edKeyStr) - if ok := db.IsHostAuthority(db.hostKeys[0].key, m.address); ok != m.good { - t.Errorf("IsHostAuthority: authority %s, address %s, wanted good = %v, got good = %v", - m.authorityFor, m.address, m.good, ok) - } - } -} - -func TestBracket(t *testing.T) { - db := testDB(t, `[git.eclipse.org]:29418,[198.41.30.196]:29418 `+edKeyStr) - - if err := db.check("git.eclipse.org:29418", &net.TCPAddr{ - IP: net.IP{198, 41, 30, 196}, - Port: 29418, - }, edKey); err != nil { - t.Errorf("got error %v, want none", err) - } - - if err := db.check("git.eclipse.org:29419", &net.TCPAddr{ - Port: 42, - }, edKey); err == nil { - t.Fatalf("no error for unknown address") - } else if ke, ok := err.(*knownhosts.KeyError); !ok { - t.Fatalf("got type %T, want *KeyError", err) - } else if len(ke.Want) > 0 { - t.Fatalf("got Want %v, want []", ke.Want) - } -} - -func TestNewKeyType(t *testing.T) { - str := fmt.Sprintf("%s %s", testAddr, edKeyStr) - db := testDB(t, str) - if err := db.check("", testAddr, ecKey); err == nil { - t.Fatalf("no error for unknown address") - } else if ke, ok := err.(*knownhosts.KeyError); !ok { - t.Fatalf("got type %T, want *KeyError", err) - } else if len(ke.Want) == 0 { - t.Fatalf("got empty KeyError.Want") - } -} - -func TestSameKeyType(t *testing.T) { - str := fmt.Sprintf("%s %s", testAddr, edKeyStr) - db := testDB(t, str) - if err := db.check("", testAddr, alternateEdKey); err == nil { - t.Fatalf("no error for unknown address") - } else if ke, ok := err.(*knownhosts.KeyError); !ok { - t.Fatalf("got type %T, want *KeyError", err) - } else if len(ke.Want) == 0 { - t.Fatalf("got empty KeyError.Want") - } else if got, want := ke.Want[0].Key.Marshal(), edKey.Marshal(); !bytes.Equal(got, want) { - t.Fatalf("got key %q, want %q", got, want) - } -} - -func TestIPAddress(t *testing.T) { - str := fmt.Sprintf("%s %s", testAddr, edKeyStr) - db := testDB(t, str) - if err := db.check("", testAddr, edKey); err != nil { - t.Errorf("got error %q, want none", err) - } -} - -func TestIPv6Address(t *testing.T) { - str := fmt.Sprintf("%s %s", testAddr6, edKeyStr) - db := testDB(t, str) - - if err := db.check("", testAddr6, edKey); err != nil { - t.Errorf("got error %q, want none", err) - } -} - -func TestBasic(t *testing.T) { - str := fmt.Sprintf("#comment\n\nserver.org,%s %s\notherhost %s", testAddr, edKeyStr, ecKeyStr) - db := testDB(t, str) - if err := db.check("server.org:22", testAddr, edKey); err != nil { - t.Errorf("got error %v, want none", err) - } - - want := knownhosts.KnownKey{ - Key: edKey, - } - if err := db.check("server.org:22", testAddr, ecKey); err == nil { - t.Errorf("succeeded, want KeyError") - } else if ke, ok := err.(*knownhosts.KeyError); !ok { - t.Errorf("got %T, want *KeyError", err) - } else if len(ke.Want) != 1 { - t.Errorf("got %v, want 1 entry", ke) - } else if !reflect.DeepEqual(ke.Want[0], want) { - t.Errorf("got %v, want %v", ke.Want[0], want) - } -} - -func TestHostNamePrecedence(t *testing.T) { - var evilAddr = &net.TCPAddr{ - IP: net.IP{66, 66, 66, 66}, - Port: 22, - } - - str := fmt.Sprintf("server.org,%s %s\nevil.org,%s %s", testAddr, edKeyStr, evilAddr, ecKeyStr) - db := testDB(t, str) - - if err := db.check("server.org:22", evilAddr, ecKey); err == nil { - t.Errorf("check succeeded") - } else if _, ok := err.(*knownhosts.KeyError); !ok { - t.Errorf("got %T, want *KeyError", err) - } -} - -func TestDBOrderingPrecedenceKeyType(t *testing.T) { - str := fmt.Sprintf("server.org,%s %s\nserver.org,%s %s", testAddr, edKeyStr, testAddr, alternateEdKeyStr) - db := testDB(t, str) - - if err := db.check("server.org:22", testAddr, alternateEdKey); err == nil { - t.Errorf("check succeeded") - } else if _, ok := err.(*knownhosts.KeyError); !ok { - t.Errorf("got %T, want *KeyError", err) - } -} - -func TestNegate(t *testing.T) { - str := fmt.Sprintf("%s,!server.org %s", testAddr, edKeyStr) - db := testDB(t, str) - if err := db.check("server.org:22", testAddr, ecKey); err == nil { - t.Errorf("succeeded") - } else if ke, ok := err.(*knownhosts.KeyError); !ok { - t.Errorf("got error type %T, want *KeyError", err) - } else if len(ke.Want) != 0 { - t.Errorf("got expected keys %d (first of type %s), want []", len(ke.Want), ke.Want[0].Key.Type()) - } -} - -func TestWildcard(t *testing.T) { - str := fmt.Sprintf("server*.domain %s", edKeyStr) - db := testDB(t, str) - - want := &knownhosts.KeyError{ - Want: []knownhosts.KnownKey{{ - Key: edKey, - }}, - } - - got := db.check("server.domain:22", &net.TCPAddr{}, ecKey) - if !reflect.DeepEqual(got, want) { - t.Errorf("got %s, want %s", got, want) - } -} - -func TestWildcardMatch(t *testing.T) { - for _, c := range []struct { - pat, str string - want bool - }{ - {"a?b", "abb", true}, - {"ab", "abc", false}, - {"abc", "ab", false}, - {"a*b", "axxxb", true}, - {"a*b", "axbxb", true}, - {"a*b", "axbxbc", false}, - {"a*?", "axbxc", true}, - {"a*b*", "axxbxxxxxx", true}, - {"a*b*c", "axxbxxxxxxc", true}, - {"a*b*?", "axxbxxxxxxc", true}, - {"a*b*z", "axxbxxbxxxz", true}, - {"a*b*z", "axxbxxzxxxz", true}, - {"a*b*z", "axxbxxzxxx", false}, - } { - got := wildcardMatch([]byte(c.pat), []byte(c.str)) - if got != c.want { - t.Errorf("wildcardMatch(%q, %q) = %v, want %v", c.pat, c.str, got, c.want) - } - - } -} - -// TODO(hanwen): test coverage for certificates. - -const testHostname = "hostname" - -// generated with keygen -H -f -const encodedTestHostnameHash = "|1|IHXZvQMvTcZTUU29+2vXFgx8Frs=|UGccIWfRVDwilMBnA3WJoRAC75Y=" - -func TestHostHash(t *testing.T) { - testHostHash(t, testHostname, encodedTestHostnameHash) -} - -func TestHashList(t *testing.T) { - encoded := knownhosts.HashHostname(testHostname) - testHostHash(t, testHostname, encoded) -} - -func testHostHash(t *testing.T, hostname, encoded string) { - typ, salt, hash, err := decodeHash(encoded) - if err != nil { - t.Fatalf("decodeHash: %v", err) - } - - if got := encodeHash(typ, salt, hash); got != encoded { - t.Errorf("got encoding %s want %s", got, encoded) - } - - if typ != sha1HashType { - t.Fatalf("got hash type %q, want %q", typ, sha1HashType) - } - - got := hashHost(hostname, salt) - if !bytes.Equal(got, hash) { - t.Errorf("got hash %x want %x", got, hash) - } -} - -func TestHashedHostkeyCheck(t *testing.T) { - str := fmt.Sprintf("%s %s", knownhosts.HashHostname(testHostname), edKeyStr) - db := testDB(t, str) - if err := db.check(testHostname+":22", testAddr, edKey); err != nil { - t.Errorf("check(%s): %v", testHostname, err) - } - want := &knownhosts.KeyError{ - Want: []knownhosts.KnownKey{{ - Key: edKey, - }}, - } - if got := db.check(testHostname+":22", testAddr, alternateEdKey); !reflect.DeepEqual(got, want) { - t.Errorf("got error %v, want %v", got, want) - } -}