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) |
||||||
|
} |
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue