Add support for ssh commit signing (#17743)
* Add support for ssh commit signing * Split out ssh verification to separate file * Show ssh key fingerprint on commit page * Update sshsig lib * Make sure we verify against correct namespace * Add ssh public key verification via ssh signatures When adding a public ssh key also validate that this user actually owns the key by signing a token with the private key. * Remove some gpg references and make verify key optional * Fix spaces indentation * Update options/locale/locale_en-US.ini Co-authored-by: Gusted <williamzijl7@hotmail.com> * Update templates/user/settings/keys_ssh.tmpl Co-authored-by: Gusted <williamzijl7@hotmail.com> * Update options/locale/locale_en-US.ini Co-authored-by: Gusted <williamzijl7@hotmail.com> * Update options/locale/locale_en-US.ini Co-authored-by: Gusted <williamzijl7@hotmail.com> * Update models/ssh_key_commit_verification.go Co-authored-by: Gusted <williamzijl7@hotmail.com> * Reword ssh/gpg_key_success message * Change Badsignature to NoKeyFound * Add sign/verify tests * Fix upstream api changes to user_model User * Match exact on SSH signature * Fix code review remarks Co-authored-by: Gusted <williamzijl7@hotmail.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: techknowlogick <techknowlogick@gitea.io>tokarchuk/v1.17
parent
f1e85622da
commit
6fe756dc93
@ -0,0 +1,77 @@ |
|||||||
|
// Copyright 2021 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 asymkey |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"fmt" |
||||||
|
"strings" |
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db" |
||||||
|
user_model "code.gitea.io/gitea/models/user" |
||||||
|
"code.gitea.io/gitea/modules/git" |
||||||
|
"code.gitea.io/gitea/modules/log" |
||||||
|
|
||||||
|
"github.com/42wim/sshsig" |
||||||
|
) |
||||||
|
|
||||||
|
// ParseCommitWithSSHSignature check if signature is good against keystore.
|
||||||
|
func ParseCommitWithSSHSignature(c *git.Commit, committer *user_model.User) *CommitVerification { |
||||||
|
// Now try to associate the signature with the committer, if present
|
||||||
|
if committer.ID != 0 { |
||||||
|
keys, err := ListPublicKeys(committer.ID, db.ListOptions{}) |
||||||
|
if err != nil { // Skipping failed to get ssh keys of user
|
||||||
|
log.Error("ListPublicKeys: %v", err) |
||||||
|
return &CommitVerification{ |
||||||
|
CommittingUser: committer, |
||||||
|
Verified: false, |
||||||
|
Reason: "gpg.error.failed_retrieval_gpg_keys", |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
committerEmailAddresses, err := user_model.GetEmailAddresses(committer.ID) |
||||||
|
if err != nil { |
||||||
|
log.Error("GetEmailAddresses: %v", err) |
||||||
|
} |
||||||
|
|
||||||
|
activated := false |
||||||
|
for _, e := range committerEmailAddresses { |
||||||
|
if e.IsActivated && strings.EqualFold(e.Email, c.Committer.Email) { |
||||||
|
activated = true |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
for _, k := range keys { |
||||||
|
if k.Verified && activated { |
||||||
|
commitVerification := verifySSHCommitVerification(c.Signature.Signature, c.Signature.Payload, k, committer, committer, c.Committer.Email) |
||||||
|
if commitVerification != nil { |
||||||
|
return commitVerification |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return &CommitVerification{ |
||||||
|
CommittingUser: committer, |
||||||
|
Verified: false, |
||||||
|
Reason: NoKeyFound, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func verifySSHCommitVerification(sig, payload string, k *PublicKey, committer, signer *user_model.User, email string) *CommitVerification { |
||||||
|
if err := sshsig.Verify(bytes.NewBuffer([]byte(payload)), []byte(sig), []byte(k.Content), "git"); err != nil { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
return &CommitVerification{ // Everything is ok
|
||||||
|
CommittingUser: committer, |
||||||
|
Verified: true, |
||||||
|
Reason: fmt.Sprintf("%s / %s", signer.Name, k.Fingerprint), |
||||||
|
SigningUser: signer, |
||||||
|
SigningSSHKey: k, |
||||||
|
SigningEmail: email, |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,49 @@ |
|||||||
|
// Copyright 2021 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 asymkey |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db" |
||||||
|
"code.gitea.io/gitea/modules/log" |
||||||
|
"github.com/42wim/sshsig" |
||||||
|
) |
||||||
|
|
||||||
|
// VerifySSHKey marks a SSH key as verified
|
||||||
|
func VerifySSHKey(ownerID int64, fingerprint, token, signature string) (string, error) { |
||||||
|
ctx, committer, err := db.TxContext() |
||||||
|
if err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
defer committer.Close() |
||||||
|
|
||||||
|
key := new(PublicKey) |
||||||
|
|
||||||
|
has, err := db.GetEngine(ctx).Where("owner_id = ? AND fingerprint = ?", ownerID, fingerprint).Get(key) |
||||||
|
if err != nil { |
||||||
|
return "", err |
||||||
|
} else if !has { |
||||||
|
return "", ErrKeyNotExist{} |
||||||
|
} |
||||||
|
|
||||||
|
if err := sshsig.Verify(bytes.NewBuffer([]byte(token)), []byte(signature), []byte(key.Content), "gitea"); err != nil { |
||||||
|
log.Error("Unable to validate token signature. Error: %v", err) |
||||||
|
return "", ErrSSHInvalidTokenSignature{ |
||||||
|
Fingerprint: key.Fingerprint, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
key.Verified = true |
||||||
|
if _, err := db.GetEngine(ctx).ID(key.ID).Cols("verified").Update(key); err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
|
||||||
|
if err := committer.Commit(); err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
|
||||||
|
return key.Fingerprint, nil |
||||||
|
} |
@ -0,0 +1,15 @@ |
|||||||
|
// Copyright 2021 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 "xorm.io/xorm" |
||||||
|
|
||||||
|
func addSSHKeyIsVerified(x *xorm.Engine) error { |
||||||
|
type PublicKey struct { |
||||||
|
Verified bool `xorm:"NOT NULL DEFAULT false"` |
||||||
|
} |
||||||
|
|
||||||
|
return x.Sync(new(PublicKey)) |
||||||
|
} |
@ -0,0 +1,201 @@ |
|||||||
|
Apache License |
||||||
|
Version 2.0, January 2004 |
||||||
|
http://www.apache.org/licenses/ |
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION |
||||||
|
|
||||||
|
1. Definitions. |
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction, |
||||||
|
and distribution as defined by Sections 1 through 9 of this document. |
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by |
||||||
|
the copyright owner that is granting the License. |
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all |
||||||
|
other entities that control, are controlled by, or are under common |
||||||
|
control with that entity. For the purposes of this definition, |
||||||
|
"control" means (i) the power, direct or indirect, to cause the |
||||||
|
direction or management of such entity, whether by contract or |
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the |
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity. |
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity |
||||||
|
exercising permissions granted by this License. |
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications, |
||||||
|
including but not limited to software source code, documentation |
||||||
|
source, and configuration files. |
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical |
||||||
|
transformation or translation of a Source form, including but |
||||||
|
not limited to compiled object code, generated documentation, |
||||||
|
and conversions to other media types. |
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or |
||||||
|
Object form, made available under the License, as indicated by a |
||||||
|
copyright notice that is included in or attached to the work |
||||||
|
(an example is provided in the Appendix below). |
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object |
||||||
|
form, that is based on (or derived from) the Work and for which the |
||||||
|
editorial revisions, annotations, elaborations, or other modifications |
||||||
|
represent, as a whole, an original work of authorship. For the purposes |
||||||
|
of this License, Derivative Works shall not include works that remain |
||||||
|
separable from, or merely link (or bind by name) to the interfaces of, |
||||||
|
the Work and Derivative Works thereof. |
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including |
||||||
|
the original version of the Work and any modifications or additions |
||||||
|
to that Work or Derivative Works thereof, that is intentionally |
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner |
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of |
||||||
|
the copyright owner. For the purposes of this definition, "submitted" |
||||||
|
means any form of electronic, verbal, or written communication sent |
||||||
|
to the Licensor or its representatives, including but not limited to |
||||||
|
communication on electronic mailing lists, source code control systems, |
||||||
|
and issue tracking systems that are managed by, or on behalf of, the |
||||||
|
Licensor for the purpose of discussing and improving the Work, but |
||||||
|
excluding communication that is conspicuously marked or otherwise |
||||||
|
designated in writing by the copyright owner as "Not a Contribution." |
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity |
||||||
|
on behalf of whom a Contribution has been received by Licensor and |
||||||
|
subsequently incorporated within the Work. |
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of |
||||||
|
this License, each Contributor hereby grants to You a perpetual, |
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable |
||||||
|
copyright license to reproduce, prepare Derivative Works of, |
||||||
|
publicly display, publicly perform, sublicense, and distribute the |
||||||
|
Work and such Derivative Works in Source or Object form. |
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of |
||||||
|
this License, each Contributor hereby grants to You a perpetual, |
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable |
||||||
|
(except as stated in this section) patent license to make, have made, |
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work, |
||||||
|
where such license applies only to those patent claims licensable |
||||||
|
by such Contributor that are necessarily infringed by their |
||||||
|
Contribution(s) alone or by combination of their Contribution(s) |
||||||
|
with the Work to which such Contribution(s) was submitted. If You |
||||||
|
institute patent litigation against any entity (including a |
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work |
||||||
|
or a Contribution incorporated within the Work constitutes direct |
||||||
|
or contributory patent infringement, then any patent licenses |
||||||
|
granted to You under this License for that Work shall terminate |
||||||
|
as of the date such litigation is filed. |
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the |
||||||
|
Work or Derivative Works thereof in any medium, with or without |
||||||
|
modifications, and in Source or Object form, provided that You |
||||||
|
meet the following conditions: |
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or |
||||||
|
Derivative Works a copy of this License; and |
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices |
||||||
|
stating that You changed the files; and |
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works |
||||||
|
that You distribute, all copyright, patent, trademark, and |
||||||
|
attribution notices from the Source form of the Work, |
||||||
|
excluding those notices that do not pertain to any part of |
||||||
|
the Derivative Works; and |
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its |
||||||
|
distribution, then any Derivative Works that You distribute must |
||||||
|
include a readable copy of the attribution notices contained |
||||||
|
within such NOTICE file, excluding those notices that do not |
||||||
|
pertain to any part of the Derivative Works, in at least one |
||||||
|
of the following places: within a NOTICE text file distributed |
||||||
|
as part of the Derivative Works; within the Source form or |
||||||
|
documentation, if provided along with the Derivative Works; or, |
||||||
|
within a display generated by the Derivative Works, if and |
||||||
|
wherever such third-party notices normally appear. The contents |
||||||
|
of the NOTICE file are for informational purposes only and |
||||||
|
do not modify the License. You may add Your own attribution |
||||||
|
notices within Derivative Works that You distribute, alongside |
||||||
|
or as an addendum to the NOTICE text from the Work, provided |
||||||
|
that such additional attribution notices cannot be construed |
||||||
|
as modifying the License. |
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and |
||||||
|
may provide additional or different license terms and conditions |
||||||
|
for use, reproduction, or distribution of Your modifications, or |
||||||
|
for any such Derivative Works as a whole, provided Your use, |
||||||
|
reproduction, and distribution of the Work otherwise complies with |
||||||
|
the conditions stated in this License. |
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise, |
||||||
|
any Contribution intentionally submitted for inclusion in the Work |
||||||
|
by You to the Licensor shall be under the terms and conditions of |
||||||
|
this License, without any additional terms or conditions. |
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify |
||||||
|
the terms of any separate license agreement you may have executed |
||||||
|
with Licensor regarding such Contributions. |
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade |
||||||
|
names, trademarks, service marks, or product names of the Licensor, |
||||||
|
except as required for reasonable and customary use in describing the |
||||||
|
origin of the Work and reproducing the content of the NOTICE file. |
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or |
||||||
|
agreed to in writing, Licensor provides the Work (and each |
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS, |
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or |
||||||
|
implied, including, without limitation, any warranties or conditions |
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A |
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the |
||||||
|
appropriateness of using or redistributing the Work and assume any |
||||||
|
risks associated with Your exercise of permissions under this License. |
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory, |
||||||
|
whether in tort (including negligence), contract, or otherwise, |
||||||
|
unless required by applicable law (such as deliberate and grossly |
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be |
||||||
|
liable to You for damages, including any direct, indirect, special, |
||||||
|
incidental, or consequential damages of any character arising as a |
||||||
|
result of this License or out of the use or inability to use the |
||||||
|
Work (including but not limited to damages for loss of goodwill, |
||||||
|
work stoppage, computer failure or malfunction, or any and all |
||||||
|
other commercial damages or losses), even if such Contributor |
||||||
|
has been advised of the possibility of such damages. |
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing |
||||||
|
the Work or Derivative Works thereof, You may choose to offer, |
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity, |
||||||
|
or other liability obligations and/or rights consistent with this |
||||||
|
License. However, in accepting such obligations, You may act only |
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf |
||||||
|
of any other Contributor, and only if You agree to indemnify, |
||||||
|
defend, and hold each Contributor harmless for any liability |
||||||
|
incurred by, or claims asserted against, such Contributor by reason |
||||||
|
of your accepting any such warranty or additional liability. |
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS |
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work. |
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following |
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]" |
||||||
|
replaced with your own identifying information. (Don't include |
||||||
|
the brackets!) The text should be enclosed in the appropriate |
||||||
|
comment syntax for the file format. We also recommend that a |
||||||
|
file or class name and description of purpose be included on the |
||||||
|
same "printed page" as the copyright notice for easier |
||||||
|
identification within third-party archives. |
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner] |
||||||
|
|
||||||
|
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. |
@ -0,0 +1,82 @@ |
|||||||
|
# Armored ssh signatures in go |
||||||
|
|
||||||
|
[![Go Reference](https://pkg.go.dev/badge/github.com/42wim/sshsig.svg)](https://pkg.go.dev/github.com/42wim/sshsig#section-documentation) |
||||||
|
|
||||||
|
Package sshsig implements signing/verifying armored SSH signatures. |
||||||
|
You can use this package to sign data and verify signatures using your ssh private keys or your ssh agent. |
||||||
|
It gives the same output as using `ssh-keygen`, eg when signing `ssh-keygen -Y sign -f keyfile -n namespace data` |
||||||
|
|
||||||
|
This code is based upon work by <https://github.com/sigstore/rekor> |
||||||
|
|
||||||
|
References: <https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.sshsig> |
||||||
|
|
||||||
|
You can find some examples on how to use this library on: <https://pkg.go.dev/github.com/42wim/sshsig#pkg-examples> |
||||||
|
|
||||||
|
## Examples |
||||||
|
|
||||||
|
```golang |
||||||
|
package main |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"fmt" |
||||||
|
"net" |
||||||
|
"os" |
||||||
|
|
||||||
|
"github.com/42wim/sshsig" |
||||||
|
"golang.org/x/crypto/ssh/agent" |
||||||
|
) |
||||||
|
|
||||||
|
func ExampleSignWithAgent() { |
||||||
|
// This example will panic when you don't have a ssh-agent running. |
||||||
|
conn, err := net.Dial("unix", os.Getenv("SSH_AUTH_SOCK")) |
||||||
|
if err != nil { |
||||||
|
panic(err) |
||||||
|
} |
||||||
|
|
||||||
|
ag := agent.NewClient(conn) |
||||||
|
|
||||||
|
// This public key must match in your agent (use `ssh-add -L` to get the public key) |
||||||
|
pubkey := []byte(`ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAo3D7CGN01tTYY/dLKXEv8RxRyxa32c51X0uKMhnMab wim@localhost`) |
||||||
|
// |
||||||
|
data := []byte("hello world") |
||||||
|
|
||||||
|
res, err := sshsig.SignWithAgent(pubkey, ag, bytes.NewBuffer(data), "file") |
||||||
|
if err != nil { |
||||||
|
panic(err) |
||||||
|
} |
||||||
|
|
||||||
|
fmt.Println(string(res)) |
||||||
|
} |
||||||
|
|
||||||
|
func ExampleSign() { |
||||||
|
privkey := []byte(`-----BEGIN OPENSSH PRIVATE KEY----- |
||||||
|
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW |
||||||
|
QyNTUxOQAAACCOjP6i4Pm/pYAAmpAMNZ6xrbHl9RW8xdul6kzIWuKMMAAAAIhoQm34aEJt |
||||||
|
+AAAAAtzc2gtZWQyNTUxOQAAACCOjP6i4Pm/pYAAmpAMNZ6xrbHl9RW8xdul6kzIWuKMMA |
||||||
|
AAAEBfIl93TLj6qHeg37GnPuZ00h8OVv1mzlhy0rhuO4Y0do6M/qLg+b+lgACakAw1nrGt |
||||||
|
seX1FbzF26XqTMha4owwAAAAAAECAwQF |
||||||
|
-----END OPENSSH PRIVATE KEY-----`) |
||||||
|
|
||||||
|
data := []byte("hello world") |
||||||
|
|
||||||
|
res, err := sshsig.Sign(privkey, bytes.NewBuffer(data), "file") |
||||||
|
if err != nil { |
||||||
|
panic(err) |
||||||
|
} |
||||||
|
|
||||||
|
fmt.Println(string(res)) |
||||||
|
|
||||||
|
// Output: |
||||||
|
// -----BEGIN SSH SIGNATURE----- |
||||||
|
// U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgjoz+ouD5v6WAAJqQDDWesa2x5f |
||||||
|
// UVvMXbpepMyFrijDAAAAAEZmlsZQAAAAAAAAAGc2hhNTEyAAAAUwAAAAtzc2gtZWQyNTUx |
||||||
|
// OQAAAEBeu9Z+vLxBORysiqEbTzJP0EZKG0/aE5HpTtvimjQS6mHZCAGFg+kimNatBE0Y1j |
||||||
|
// gS4pfD73TlML1SyB5lb/YO |
||||||
|
// -----END SSH SIGNATURE----- |
||||||
|
} |
||||||
|
|
||||||
|
func main() { |
||||||
|
ExampleSign() |
||||||
|
} |
||||||
|
``` |
@ -0,0 +1,11 @@ |
|||||||
|
/* |
||||||
|
Package sshsig implements signing/verifying armored SSH signatures. |
||||||
|
You can use this package to sign data and verify signatures using your ssh private keys or your ssh agent. |
||||||
|
It gives the same output as using `ssh-keygen`, eg when signing `ssh-keygen -Y sign -f keyfile -n namespace data` |
||||||
|
|
||||||
|
This code is based upon work by https://github.com/sigstore/rekor
|
||||||
|
|
||||||
|
References: |
||||||
|
- https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.sshsig
|
||||||
|
*/ |
||||||
|
package sshsig |
@ -0,0 +1,95 @@ |
|||||||
|
// Modified by 42wim
|
||||||
|
//
|
||||||
|
// Copyright 2021 The Sigstore Authors.
|
||||||
|
//
|
||||||
|
// 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 sshsig |
||||||
|
|
||||||
|
import ( |
||||||
|
"errors" |
||||||
|
"fmt" |
||||||
|
|
||||||
|
"github.com/42wim/sshsig/pem" |
||||||
|
|
||||||
|
"golang.org/x/crypto/ssh" |
||||||
|
) |
||||||
|
|
||||||
|
const ( |
||||||
|
pemType = "SSH SIGNATURE" |
||||||
|
) |
||||||
|
|
||||||
|
// Armored returns the signature in an armored format.
|
||||||
|
func Armor(s *ssh.Signature, p ssh.PublicKey, ns string) []byte { |
||||||
|
sig := WrappedSig{ |
||||||
|
Version: 1, |
||||||
|
PublicKey: string(p.Marshal()), |
||||||
|
Namespace: ns, |
||||||
|
HashAlgorithm: defaultHashAlgorithm, |
||||||
|
Signature: string(ssh.Marshal(s)), |
||||||
|
} |
||||||
|
|
||||||
|
copy(sig.MagicHeader[:], magicHeader) |
||||||
|
|
||||||
|
enc := pem.EncodeToMemory(&pem.Block{ |
||||||
|
Type: pemType, |
||||||
|
Bytes: ssh.Marshal(sig), |
||||||
|
}) |
||||||
|
return enc |
||||||
|
} |
||||||
|
|
||||||
|
// Decode parses an armored signature.
|
||||||
|
func Decode(b []byte) (*Signature, error) { |
||||||
|
pemBlock, _ := pem.Decode(b) |
||||||
|
if pemBlock == nil { |
||||||
|
return nil, errors.New("unable to decode pem file") |
||||||
|
} |
||||||
|
|
||||||
|
if pemBlock.Type != pemType { |
||||||
|
return nil, fmt.Errorf("wrong pem block type: %s. Expected SSH-SIGNATURE", pemBlock.Type) |
||||||
|
} |
||||||
|
|
||||||
|
// Now we unmarshal it into the Signature block
|
||||||
|
sig := WrappedSig{} |
||||||
|
if err := ssh.Unmarshal(pemBlock.Bytes, &sig); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
if sig.Version != 1 { |
||||||
|
return nil, fmt.Errorf("unsupported signature version: %d", sig.Version) |
||||||
|
} |
||||||
|
if string(sig.MagicHeader[:]) != magicHeader { |
||||||
|
return nil, fmt.Errorf("invalid magic header: %s", sig.MagicHeader[:]) |
||||||
|
} |
||||||
|
if _, ok := supportedHashAlgorithms[sig.HashAlgorithm]; !ok { |
||||||
|
return nil, fmt.Errorf("unsupported hash algorithm: %s", sig.HashAlgorithm) |
||||||
|
} |
||||||
|
|
||||||
|
// Now we can unpack the Signature and PublicKey blocks
|
||||||
|
sshSig := ssh.Signature{} |
||||||
|
if err := ssh.Unmarshal([]byte(sig.Signature), &sshSig); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
// TODO: check the format here (should be rsa-sha512)
|
||||||
|
|
||||||
|
pk, err := ssh.ParsePublicKey([]byte(sig.PublicKey)) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
return &Signature{ |
||||||
|
signature: &sshSig, |
||||||
|
pk: pk, |
||||||
|
hashAlg: sig.HashAlgorithm, |
||||||
|
}, nil |
||||||
|
} |
@ -0,0 +1,7 @@ |
|||||||
|
module github.com/42wim/sshsig |
||||||
|
|
||||||
|
go 1.17 |
||||||
|
|
||||||
|
require golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 |
||||||
|
|
||||||
|
require golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1 // indirect |
@ -0,0 +1,12 @@ |
|||||||
|
golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 h1:/pEO3GD/ABYAjuakUS6xSEmmlyVS4kxBNkeA9tLJiTI= |
||||||
|
golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= |
||||||
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= |
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
||||||
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= |
||||||
|
golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1 h1:kwrAHlwJ0DUBZwQ238v+Uod/3eZ8B2K5rYsUHBQvzmI= |
||||||
|
golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= |
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= |
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= |
||||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= |
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= |
@ -0,0 +1,346 @@ |
|||||||
|
// Modified by 42wim
|
||||||
|
//
|
||||||
|
// Copyright 2009 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.
|
||||||
|
|
||||||
|
// Package pem implements the PEM data encoding, which originated in Privacy
|
||||||
|
// Enhanced Mail. The most common use of PEM encoding today is in TLS keys and
|
||||||
|
// certificates. See RFC 1421.
|
||||||
|
package pem |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"encoding/base64" |
||||||
|
"errors" |
||||||
|
"io" |
||||||
|
"sort" |
||||||
|
"strings" |
||||||
|
) |
||||||
|
|
||||||
|
// A Block represents a PEM encoded structure.
|
||||||
|
//
|
||||||
|
// The encoded form is:
|
||||||
|
// -----BEGIN Type-----
|
||||||
|
// Headers
|
||||||
|
// base64-encoded Bytes
|
||||||
|
// -----END Type-----
|
||||||
|
// where Headers is a possibly empty sequence of Key: Value lines.
|
||||||
|
type Block struct { |
||||||
|
Type string // The type, taken from the preamble (i.e. "RSA PRIVATE KEY").
|
||||||
|
Headers map[string]string // Optional headers.
|
||||||
|
Bytes []byte // The decoded bytes of the contents. Typically a DER encoded ASN.1 structure.
|
||||||
|
} |
||||||
|
|
||||||
|
// getLine results the first \r\n or \n delineated line from the given byte
|
||||||
|
// array. The line does not include trailing whitespace or the trailing new
|
||||||
|
// line bytes. The remainder of the byte array (also not including the new line
|
||||||
|
// bytes) is also returned and this will always be smaller than the original
|
||||||
|
// argument.
|
||||||
|
func getLine(data []byte) (line, rest []byte) { |
||||||
|
i := bytes.IndexByte(data, '\n') |
||||||
|
var j int |
||||||
|
if i < 0 { |
||||||
|
i = len(data) |
||||||
|
j = i |
||||||
|
} else { |
||||||
|
j = i + 1 |
||||||
|
if i > 0 && data[i-1] == '\r' { |
||||||
|
i-- |
||||||
|
} |
||||||
|
} |
||||||
|
return bytes.TrimRight(data[0:i], " \t"), data[j:] |
||||||
|
} |
||||||
|
|
||||||
|
// removeSpacesAndTabs returns a copy of its input with all spaces and tabs
|
||||||
|
// removed, if there were any. Otherwise, the input is returned unchanged.
|
||||||
|
//
|
||||||
|
// The base64 decoder already skips newline characters, so we don't need to
|
||||||
|
// filter them out here.
|
||||||
|
func removeSpacesAndTabs(data []byte) []byte { |
||||||
|
if !bytes.ContainsAny(data, " \t") { |
||||||
|
// Fast path; most base64 data within PEM contains newlines, but
|
||||||
|
// no spaces nor tabs. Skip the extra alloc and work.
|
||||||
|
return data |
||||||
|
} |
||||||
|
result := make([]byte, len(data)) |
||||||
|
n := 0 |
||||||
|
|
||||||
|
for _, b := range data { |
||||||
|
if b == ' ' || b == '\t' { |
||||||
|
continue |
||||||
|
} |
||||||
|
result[n] = b |
||||||
|
n++ |
||||||
|
} |
||||||
|
|
||||||
|
return result[0:n] |
||||||
|
} |
||||||
|
|
||||||
|
var ( |
||||||
|
pemStart = []byte("\n-----BEGIN ") |
||||||
|
pemEnd = []byte("\n-----END ") |
||||||
|
pemEndOfLine = []byte("-----") |
||||||
|
) |
||||||
|
|
||||||
|
// Decode will find the next PEM formatted block (certificate, private key
|
||||||
|
// etc) in the input. It returns that block and the remainder of the input. If
|
||||||
|
// no PEM data is found, p is nil and the whole of the input is returned in
|
||||||
|
// rest.
|
||||||
|
func Decode(data []byte) (p *Block, rest []byte) { |
||||||
|
// pemStart begins with a newline. However, at the very beginning of
|
||||||
|
// the byte array, we'll accept the start string without it.
|
||||||
|
rest = data |
||||||
|
if bytes.HasPrefix(data, pemStart[1:]) { |
||||||
|
rest = rest[len(pemStart)-1 : len(data)] |
||||||
|
} else if i := bytes.Index(data, pemStart); i >= 0 { |
||||||
|
rest = rest[i+len(pemStart) : len(data)] |
||||||
|
} else { |
||||||
|
return nil, data |
||||||
|
} |
||||||
|
|
||||||
|
typeLine, rest := getLine(rest) |
||||||
|
if !bytes.HasSuffix(typeLine, pemEndOfLine) { |
||||||
|
return decodeError(data, rest) |
||||||
|
} |
||||||
|
typeLine = typeLine[0 : len(typeLine)-len(pemEndOfLine)] |
||||||
|
|
||||||
|
p = &Block{ |
||||||
|
Headers: make(map[string]string), |
||||||
|
Type: string(typeLine), |
||||||
|
} |
||||||
|
|
||||||
|
for { |
||||||
|
// This loop terminates because getLine's second result is
|
||||||
|
// always smaller than its argument.
|
||||||
|
if len(rest) == 0 { |
||||||
|
return nil, data |
||||||
|
} |
||||||
|
line, next := getLine(rest) |
||||||
|
|
||||||
|
i := bytes.IndexByte(line, ':') |
||||||
|
if i == -1 { |
||||||
|
break |
||||||
|
} |
||||||
|
|
||||||
|
// TODO(agl): need to cope with values that spread across lines.
|
||||||
|
key, val := line[:i], line[i+1:] |
||||||
|
key = bytes.TrimSpace(key) |
||||||
|
val = bytes.TrimSpace(val) |
||||||
|
p.Headers[string(key)] = string(val) |
||||||
|
rest = next |
||||||
|
} |
||||||
|
|
||||||
|
var endIndex, endTrailerIndex int |
||||||
|
|
||||||
|
// If there were no headers, the END line might occur
|
||||||
|
// immediately, without a leading newline.
|
||||||
|
if len(p.Headers) == 0 && bytes.HasPrefix(rest, pemEnd[1:]) { |
||||||
|
endIndex = 0 |
||||||
|
endTrailerIndex = len(pemEnd) - 1 |
||||||
|
} else { |
||||||
|
endIndex = bytes.Index(rest, pemEnd) |
||||||
|
endTrailerIndex = endIndex + len(pemEnd) |
||||||
|
} |
||||||
|
|
||||||
|
if endIndex < 0 { |
||||||
|
return decodeError(data, rest) |
||||||
|
} |
||||||
|
|
||||||
|
// After the "-----" of the ending line, there should be the same type
|
||||||
|
// and then a final five dashes.
|
||||||
|
endTrailer := rest[endTrailerIndex:] |
||||||
|
endTrailerLen := len(typeLine) + len(pemEndOfLine) |
||||||
|
if len(endTrailer) < endTrailerLen { |
||||||
|
return decodeError(data, rest) |
||||||
|
} |
||||||
|
|
||||||
|
restOfEndLine := endTrailer[endTrailerLen:] |
||||||
|
endTrailer = endTrailer[:endTrailerLen] |
||||||
|
if !bytes.HasPrefix(endTrailer, typeLine) || |
||||||
|
!bytes.HasSuffix(endTrailer, pemEndOfLine) { |
||||||
|
return decodeError(data, rest) |
||||||
|
} |
||||||
|
|
||||||
|
// The line must end with only whitespace.
|
||||||
|
if s, _ := getLine(restOfEndLine); len(s) != 0 { |
||||||
|
return decodeError(data, rest) |
||||||
|
} |
||||||
|
|
||||||
|
base64Data := removeSpacesAndTabs(rest[:endIndex]) |
||||||
|
p.Bytes = make([]byte, base64.StdEncoding.DecodedLen(len(base64Data))) |
||||||
|
n, err := base64.StdEncoding.Decode(p.Bytes, base64Data) |
||||||
|
if err != nil { |
||||||
|
return decodeError(data, rest) |
||||||
|
} |
||||||
|
p.Bytes = p.Bytes[:n] |
||||||
|
|
||||||
|
// the -1 is because we might have only matched pemEnd without the
|
||||||
|
// leading newline if the PEM block was empty.
|
||||||
|
_, rest = getLine(rest[endIndex+len(pemEnd)-1:]) |
||||||
|
|
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
func decodeError(data, rest []byte) (*Block, []byte) { |
||||||
|
// If we get here then we have rejected a likely looking, but
|
||||||
|
// ultimately invalid PEM block. We need to start over from a new
|
||||||
|
// position. We have consumed the preamble line and will have consumed
|
||||||
|
// any lines which could be header lines. However, a valid preamble
|
||||||
|
// line is not a valid header line, therefore we cannot have consumed
|
||||||
|
// the preamble line for the any subsequent block. Thus, we will always
|
||||||
|
// find any valid block, no matter what bytes precede it.
|
||||||
|
//
|
||||||
|
// For example, if the input is
|
||||||
|
//
|
||||||
|
// -----BEGIN MALFORMED BLOCK-----
|
||||||
|
// junk that may look like header lines
|
||||||
|
// or data lines, but no END line
|
||||||
|
//
|
||||||
|
// -----BEGIN ACTUAL BLOCK-----
|
||||||
|
// realdata
|
||||||
|
// -----END ACTUAL BLOCK-----
|
||||||
|
//
|
||||||
|
// we've failed to parse using the first BEGIN line
|
||||||
|
// and now will try again, using the second BEGIN line.
|
||||||
|
p, rest := Decode(rest) |
||||||
|
if p == nil { |
||||||
|
rest = data |
||||||
|
} |
||||||
|
return p, rest |
||||||
|
} |
||||||
|
|
||||||
|
// https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.sshsig#L20-L21
|
||||||
|
// Should be on 76 chars, but strangely enough ssh breaks on 70
|
||||||
|
const pemLineLength = 70 |
||||||
|
|
||||||
|
type lineBreaker struct { |
||||||
|
line [pemLineLength]byte |
||||||
|
used int |
||||||
|
out io.Writer |
||||||
|
} |
||||||
|
|
||||||
|
var nl = []byte{'\n'} |
||||||
|
|
||||||
|
func (l *lineBreaker) Write(b []byte) (n int, err error) { |
||||||
|
if l.used+len(b) < pemLineLength { |
||||||
|
copy(l.line[l.used:], b) |
||||||
|
l.used += len(b) |
||||||
|
return len(b), nil |
||||||
|
} |
||||||
|
|
||||||
|
n, err = l.out.Write(l.line[0:l.used]) |
||||||
|
if err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
excess := pemLineLength - l.used |
||||||
|
l.used = 0 |
||||||
|
|
||||||
|
n, err = l.out.Write(b[0:excess]) |
||||||
|
if err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
n, err = l.out.Write(nl) |
||||||
|
if err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
return l.Write(b[excess:]) |
||||||
|
} |
||||||
|
|
||||||
|
func (l *lineBreaker) Close() (err error) { |
||||||
|
if l.used > 0 { |
||||||
|
_, err = l.out.Write(l.line[0:l.used]) |
||||||
|
if err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
_, err = l.out.Write(nl) |
||||||
|
} |
||||||
|
|
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
func writeHeader(out io.Writer, k, v string) error { |
||||||
|
_, err := out.Write([]byte(k + ": " + v + "\n")) |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// Encode writes the PEM encoding of b to out.
|
||||||
|
func Encode(out io.Writer, b *Block) error { |
||||||
|
// Check for invalid block before writing any output.
|
||||||
|
for k := range b.Headers { |
||||||
|
if strings.Contains(k, ":") { |
||||||
|
return errors.New("pem: cannot encode a header key that contains a colon") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// All errors below are relayed from underlying io.Writer,
|
||||||
|
// so it is now safe to write data.
|
||||||
|
|
||||||
|
if _, err := out.Write(pemStart[1:]); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
if _, err := out.Write([]byte(b.Type + "-----\n")); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
if len(b.Headers) > 0 { |
||||||
|
const procType = "Proc-Type" |
||||||
|
h := make([]string, 0, len(b.Headers)) |
||||||
|
hasProcType := false |
||||||
|
for k := range b.Headers { |
||||||
|
if k == procType { |
||||||
|
hasProcType = true |
||||||
|
continue |
||||||
|
} |
||||||
|
h = append(h, k) |
||||||
|
} |
||||||
|
// The Proc-Type header must be written first.
|
||||||
|
// See RFC 1421, section 4.6.1.1
|
||||||
|
if hasProcType { |
||||||
|
if err := writeHeader(out, procType, b.Headers[procType]); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
// For consistency of output, write other headers sorted by key.
|
||||||
|
sort.Strings(h) |
||||||
|
for _, k := range h { |
||||||
|
if err := writeHeader(out, k, b.Headers[k]); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
if _, err := out.Write(nl); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
var breaker lineBreaker |
||||||
|
breaker.out = out |
||||||
|
|
||||||
|
b64 := base64.NewEncoder(base64.StdEncoding, &breaker) |
||||||
|
if _, err := b64.Write(b.Bytes); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
b64.Close() |
||||||
|
breaker.Close() |
||||||
|
|
||||||
|
if _, err := out.Write(pemEnd[1:]); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
_, err := out.Write([]byte(b.Type + "-----\n")) |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// EncodeToMemory returns the PEM encoding of b.
|
||||||
|
//
|
||||||
|
// If b has invalid headers and cannot be encoded,
|
||||||
|
// EncodeToMemory returns nil. If it is important to
|
||||||
|
// report details about this error case, use Encode instead.
|
||||||
|
func EncodeToMemory(b *Block) []byte { |
||||||
|
var buf bytes.Buffer |
||||||
|
if err := Encode(&buf, b); err != nil { |
||||||
|
return nil |
||||||
|
} |
||||||
|
return buf.Bytes() |
||||||
|
} |
@ -0,0 +1,169 @@ |
|||||||
|
// Modifications by 42wim
|
||||||
|
//
|
||||||
|
// Copyright 2021 The Sigstore Authors.
|
||||||
|
//
|
||||||
|
// 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 sshsig |
||||||
|
|
||||||
|
import ( |
||||||
|
"crypto/rand" |
||||||
|
"crypto/sha256" |
||||||
|
"crypto/sha512" |
||||||
|
"errors" |
||||||
|
"hash" |
||||||
|
"io" |
||||||
|
|
||||||
|
"golang.org/x/crypto/ssh" |
||||||
|
"golang.org/x/crypto/ssh/agent" |
||||||
|
) |
||||||
|
|
||||||
|
// https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.sshsig#L81
|
||||||
|
type MessageWrapper struct { |
||||||
|
Namespace string |
||||||
|
Reserved string |
||||||
|
HashAlgorithm string |
||||||
|
Hash string |
||||||
|
} |
||||||
|
|
||||||
|
// https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.sshsig#L34
|
||||||
|
type WrappedSig struct { |
||||||
|
MagicHeader [6]byte |
||||||
|
Version uint32 |
||||||
|
PublicKey string |
||||||
|
Namespace string |
||||||
|
Reserved string |
||||||
|
HashAlgorithm string |
||||||
|
Signature string |
||||||
|
} |
||||||
|
|
||||||
|
const ( |
||||||
|
magicHeader = "SSHSIG" |
||||||
|
defaultHashAlgorithm = "sha512" |
||||||
|
) |
||||||
|
|
||||||
|
var supportedHashAlgorithms = map[string]func() hash.Hash{ |
||||||
|
"sha256": sha256.New, |
||||||
|
"sha512": sha512.New, |
||||||
|
} |
||||||
|
|
||||||
|
func wrapData(m io.Reader, ns string) ([]byte, error) { |
||||||
|
hf := sha512.New() |
||||||
|
if _, err := io.Copy(hf, m); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
mh := hf.Sum(nil) |
||||||
|
|
||||||
|
sp := MessageWrapper{ |
||||||
|
Namespace: ns, |
||||||
|
HashAlgorithm: defaultHashAlgorithm, |
||||||
|
Hash: string(mh), |
||||||
|
} |
||||||
|
|
||||||
|
dataMessageWrapper := ssh.Marshal(sp) |
||||||
|
dataMessageWrapper = append([]byte(magicHeader), dataMessageWrapper...) |
||||||
|
|
||||||
|
return dataMessageWrapper, nil |
||||||
|
} |
||||||
|
|
||||||
|
func sign(s ssh.AlgorithmSigner, m io.Reader, ns string) (*ssh.Signature, error) { |
||||||
|
dataMessageWrapper, err := wrapData(m, ns) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
// ssh-rsa is not supported for RSA keys:
|
||||||
|
// https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.sshsig#L71
|
||||||
|
// We can use the default value of "" for other key types though.
|
||||||
|
algo := "" |
||||||
|
if s.PublicKey().Type() == ssh.KeyAlgoRSA { |
||||||
|
algo = ssh.SigAlgoRSASHA2512 |
||||||
|
} |
||||||
|
|
||||||
|
return s.SignWithAlgorithm(rand.Reader, dataMessageWrapper, algo) |
||||||
|
} |
||||||
|
|
||||||
|
func signAgent(pk ssh.PublicKey, ag agent.Agent, m io.Reader, ns string) (*ssh.Signature, error) { |
||||||
|
dataMessageWrapper, err := wrapData(m, ns) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
var sigFlag agent.SignatureFlags |
||||||
|
if pk.Type() == ssh.KeyAlgoRSA { |
||||||
|
sigFlag = agent.SignatureFlagRsaSha512 |
||||||
|
} |
||||||
|
|
||||||
|
agExt, ok := ag.(agent.ExtendedAgent) |
||||||
|
if !ok { |
||||||
|
return nil, errors.New("couldn't cast to ExtendedAgent") |
||||||
|
} |
||||||
|
|
||||||
|
return agExt.SignWithFlags(pk, dataMessageWrapper, sigFlag) |
||||||
|
} |
||||||
|
|
||||||
|
// SignWithAgent asks the ssh Agent to sign the data with the signer matching the given publicKey and returns an armored signature.
|
||||||
|
// The purpose of the namespace value is to specify a unambiguous
|
||||||
|
// interpretation domain for the signature, e.g. file signing.
|
||||||
|
// This prevents cross-protocol attacks caused by signatures
|
||||||
|
// intended for one intended domain being accepted in another.
|
||||||
|
// If empty, the default is "file".
|
||||||
|
// This can be compared with `ssh-keygen -Y sign -f keyfile -n namespace data`
|
||||||
|
func SignWithAgent(publicKey []byte, ag agent.Agent, data io.Reader, namespace string) ([]byte, error) { |
||||||
|
pk, _, _, _, err := ssh.ParseAuthorizedKey(publicKey) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
if namespace == "" { |
||||||
|
namespace = defaultNamespace |
||||||
|
} |
||||||
|
|
||||||
|
sig, err := signAgent(pk, ag, data, namespace) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
armored := Armor(sig, pk, namespace) |
||||||
|
return armored, nil |
||||||
|
} |
||||||
|
|
||||||
|
// Sign signs the data with the given private key in PEM format and returns an armored signature.
|
||||||
|
// The purpose of the namespace value is to specify a unambiguous
|
||||||
|
// interpretation domain for the signature, e.g. file signing.
|
||||||
|
// This prevents cross-protocol attacks caused by signatures
|
||||||
|
// intended for one intended domain being accepted in another.
|
||||||
|
// If empty, the default is "file".
|
||||||
|
// This can be compared with `ssh-keygen -Y sign -f keyfile -n namespace data`
|
||||||
|
func Sign(pemBytes []byte, data io.Reader, namespace string) ([]byte, error) { |
||||||
|
s, err := ssh.ParsePrivateKey(pemBytes) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
as, ok := s.(ssh.AlgorithmSigner) |
||||||
|
if !ok { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
if namespace == "" { |
||||||
|
namespace = defaultNamespace |
||||||
|
} |
||||||
|
|
||||||
|
sig, err := sign(as, data, namespace) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
armored := Armor(sig, s.PublicKey(), namespace) |
||||||
|
return armored, nil |
||||||
|
} |
@ -0,0 +1,29 @@ |
|||||||
|
// modified by 42wim
|
||||||
|
//
|
||||||
|
// Copyright 2021 The Sigstore Authors.
|
||||||
|
//
|
||||||
|
// 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 sshsig |
||||||
|
|
||||||
|
import ( |
||||||
|
"golang.org/x/crypto/ssh" |
||||||
|
) |
||||||
|
|
||||||
|
type Signature struct { |
||||||
|
signature *ssh.Signature |
||||||
|
pk ssh.PublicKey |
||||||
|
hashAlg string |
||||||
|
} |
||||||
|
|
||||||
|
const defaultNamespace = "file" |
@ -0,0 +1,56 @@ |
|||||||
|
//
|
||||||
|
// Copyright 2021 The Sigstore Authors.
|
||||||
|
//
|
||||||
|
// 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 sshsig |
||||||
|
|
||||||
|
import ( |
||||||
|
"io" |
||||||
|
|
||||||
|
"golang.org/x/crypto/ssh" |
||||||
|
) |
||||||
|
|
||||||
|
// Verify verifies the signature of the given data and the armored signature using the given public key and the namespace.
|
||||||
|
// If the namespace is empty, the default namespace (file) is used.
|
||||||
|
func Verify(message io.Reader, armoredSignature []byte, publicKey []byte, namespace string) error { |
||||||
|
if namespace == "" { |
||||||
|
namespace = defaultNamespace |
||||||
|
} |
||||||
|
|
||||||
|
decodedSignature, err := Decode(armoredSignature) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
desiredPk, _, _, _, err := ssh.ParseAuthorizedKey(publicKey) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// Hash the message so we can verify it against the signature.
|
||||||
|
h := supportedHashAlgorithms[decodedSignature.hashAlg]() |
||||||
|
if _, err := io.Copy(h, message); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
hm := h.Sum(nil) |
||||||
|
|
||||||
|
toVerify := MessageWrapper{ |
||||||
|
Namespace: namespace, |
||||||
|
HashAlgorithm: decodedSignature.hashAlg, |
||||||
|
Hash: string(hm), |
||||||
|
} |
||||||
|
signedMessage := ssh.Marshal(toVerify) |
||||||
|
signedMessage = append([]byte(magicHeader), signedMessage...) |
||||||
|
return desiredPk.Verify(signedMessage, decodedSignature.signature) |
||||||
|
} |
@ -0,0 +1,14 @@ |
|||||||
|
// Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT.
|
||||||
|
|
||||||
|
// Copyright 2021 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.
|
||||||
|
|
||||||
|
//go:build go1.18
|
||||||
|
// +build go1.18
|
||||||
|
|
||||||
|
package idna |
||||||
|
|
||||||
|
// Transitional processing is disabled by default in Go 1.18.
|
||||||
|
// https://golang.org/issue/47510
|
||||||
|
const transitionalLookup = false |
@ -0,0 +1,12 @@ |
|||||||
|
// Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT.
|
||||||
|
|
||||||
|
// Copyright 2021 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.
|
||||||
|
|
||||||
|
//go:build !go1.18
|
||||||
|
// +build !go1.18
|
||||||
|
|
||||||
|
package idna |
||||||
|
|
||||||
|
const transitionalLookup = true |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,21 @@ |
|||||||
|
// Copyright 2021 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.
|
||||||
|
|
||||||
|
//go:build linux
|
||||||
|
// +build linux
|
||||||
|
|
||||||
|
package unix |
||||||
|
|
||||||
|
import "runtime" |
||||||
|
|
||||||
|
// SysvShmCtl performs control operations on the shared memory segment
|
||||||
|
// specified by id.
|
||||||
|
func SysvShmCtl(id, cmd int, desc *SysvShmDesc) (result int, err error) { |
||||||
|
if runtime.GOARCH == "arm" || |
||||||
|
runtime.GOARCH == "mips64" || runtime.GOARCH == "mips64le" { |
||||||
|
cmd |= ipc_64 |
||||||
|
} |
||||||
|
|
||||||
|
return shmctl(id, cmd, desc) |
||||||
|
} |
@ -0,0 +1,61 @@ |
|||||||
|
// Copyright 2021 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.
|
||||||
|
|
||||||
|
//go:build (darwin && !ios) || linux
|
||||||
|
// +build darwin,!ios linux
|
||||||
|
|
||||||
|
package unix |
||||||
|
|
||||||
|
import ( |
||||||
|
"unsafe" |
||||||
|
|
||||||
|
"golang.org/x/sys/internal/unsafeheader" |
||||||
|
) |
||||||
|
|
||||||
|
// SysvShmAttach attaches the Sysv shared memory segment associated with the
|
||||||
|
// shared memory identifier id.
|
||||||
|
func SysvShmAttach(id int, addr uintptr, flag int) ([]byte, error) { |
||||||
|
addr, errno := shmat(id, addr, flag) |
||||||
|
if errno != nil { |
||||||
|
return nil, errno |
||||||
|
} |
||||||
|
|
||||||
|
// Retrieve the size of the shared memory to enable slice creation
|
||||||
|
var info SysvShmDesc |
||||||
|
|
||||||
|
_, err := SysvShmCtl(id, IPC_STAT, &info) |
||||||
|
if err != nil { |
||||||
|
// release the shared memory if we can't find the size
|
||||||
|
|
||||||
|
// ignoring error from shmdt as there's nothing sensible to return here
|
||||||
|
shmdt(addr) |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
// Use unsafe to convert addr into a []byte.
|
||||||
|
// TODO: convert to unsafe.Slice once we can assume Go 1.17
|
||||||
|
var b []byte |
||||||
|
hdr := (*unsafeheader.Slice)(unsafe.Pointer(&b)) |
||||||
|
hdr.Data = unsafe.Pointer(addr) |
||||||
|
hdr.Cap = int(info.Segsz) |
||||||
|
hdr.Len = int(info.Segsz) |
||||||
|
return b, nil |
||||||
|
} |
||||||
|
|
||||||
|
// SysvShmDetach unmaps the shared memory slice returned from SysvShmAttach.
|
||||||
|
//
|
||||||
|
// It is not safe to use the slice after calling this function.
|
||||||
|
func SysvShmDetach(data []byte) error { |
||||||
|
if len(data) == 0 { |
||||||
|
return EINVAL |
||||||
|
} |
||||||
|
|
||||||
|
return shmdt(uintptr(unsafe.Pointer(&data[0]))) |
||||||
|
} |
||||||
|
|
||||||
|
// SysvShmGet returns the Sysv shared memory identifier associated with key.
|
||||||
|
// If the IPC_CREAT flag is specified a new segment is created.
|
||||||
|
func SysvShmGet(key, size, flag int) (id int, err error) { |
||||||
|
return shmget(key, size, flag) |
||||||
|
} |
@ -0,0 +1,14 @@ |
|||||||
|
// Copyright 2021 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.
|
||||||
|
|
||||||
|
//go:build darwin && !ios
|
||||||
|
// +build darwin,!ios
|
||||||
|
|
||||||
|
package unix |
||||||
|
|
||||||
|
// SysvShmCtl performs control operations on the shared memory segment
|
||||||
|
// specified by id.
|
||||||
|
func SysvShmCtl(id, cmd int, desc *SysvShmDesc) (result int, err error) { |
||||||
|
return shmctl(id, cmd, desc) |
||||||
|
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue