1
0
Fork 0

Adding upstream version 0.0~git20250502.5100632.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-05-18 16:24:40 +02:00
parent bdc5bee1be
commit 879cf0a009
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
16 changed files with 1453 additions and 0 deletions

201
LICENSE Normal file
View file

@ -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.

82
README.md Normal file
View file

@ -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()
}
```

11
doc.go Normal file
View file

@ -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

95
encode.go Normal file
View file

@ -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
}

60
example_test.go Normal file
View file

@ -0,0 +1,60 @@
package sshsig_test
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-----
}

7
go.mod Normal file
View file

@ -0,0 +1,7 @@
module github.com/42wim/sshsig
go 1.23.0
require golang.org/x/crypto v0.37.0
require golang.org/x/sys v0.32.0 // indirect

6
go.sum Normal file
View file

@ -0,0 +1,6 @@
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=

346
pem/encode.go Normal file
View file

@ -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()
}

169
sign.go Normal file
View file

@ -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
}

332
sign_test.go Normal file
View file

@ -0,0 +1,332 @@
// 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 (
"bytes"
"io/ioutil"
"os/exec"
"path/filepath"
"strings"
"testing"
)
var (
// Generated with "ssh-keygen -C test@rekor.dev -f id_rsa"
sshPrivateKey = `-----BEGIN OPENSSH PRIVATE KEY-----
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
-----END OPENSSH PRIVATE KEY-----
`
sshPublicKey = `ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDXofkiahE7uavjWvxnwkUF27qMgz7pdTwzSv0XzVG6EtirOv3PDWct4YKoXE9c0EqbxnIfYEKwEextdvB7zkgwczdJSHxf/18jQumLn/FuoCmugVSk1H5Qli/qzwBpaTnOk3WuakGuoYUl8ZAokKKgOKLA0aZJ1WRQ2ZCZggA3EkwNZiY17y9Q6HqdgQcH6XN8aAMADNVJdMAJb33hSRJjjsAPTmzBTishP8lYDoGRSsSE7/8XRBCEV5E4I8mI9GElcZwV/1KJx98mpH8QvMzXM1idFcwPRtt1NTAOshwgUU0Fu1x8lU5RQIa6ZKW36qNQLvLxy/BscC7B/mdLptoDs/ot9NimUXZcgCR1a2Q3o7Wi6jIgcgJcyV10Nba81ol4RdN4qPHnVZIzuo+dBkqwG3CMtB4Rj84+Qi+7zyU01hIPreoxQDXaayiGPBUUIiAlW9gsiuRWJzNnu3cvuWDLVfQIkjh7Wug58z+v2NOJ7IMdyERillhzDcvVHaq14+U= test@rekor.dev
`
// Generated with "ssh-keygen -C other-test@rekor.dev -f id_rsa"
otherSSHPrivateKey = `-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
NhAAAAAwEAAQAAAYEAw/WCSWC9TEvCQOwO+T68EvNa3OSIv1Y0+sT8uSvyjPyEO0+p0t8C
g/zy67vOxiQpU5jN6MItjXAjMmeCm8GKMt6gk+cDoaAev/ZfjuzSL7RayExpmhBleh2X3G
KLkkXF9ABFNchlTqSLOZiEjDoNpbFv16KT1sE6CqW8DjxXQkQk9JK65hLH+BxeWMNCEJVa
Cma4X04aJmC7zJAi5yGeeT0SKVqMohavF90O6XiYFCQHuwXPPyHfocqgudmXnozz+6D6ax
JKZMwQsNp3WKumOjlzWnxBCCB1l2jN6Rag8aJ2277iMFXRwjTL/8jaEsW4KkysDf0GjV2/
iqbr0q5b0arDYbv7CrGBR+uH0wGz/Zog1x5iZANObhZULpDrLVJidEMc27HXBb7PMsNDy7
BGYRB1yc0d0y83p8mUqvOlWSArxn1WnAZO04pAgTrclrhEh4ZXOkn2Sn82eu3DpQ8inkol
Y4IfnhIfbOIeemoUNq1tOUquhow9GLRM6INieHLBAAAFkPPnA1jz5wNYAAAAB3NzaC1yc2
EAAAGBAMP1gklgvUxLwkDsDvk+vBLzWtzkiL9WNPrE/Lkr8oz8hDtPqdLfAoP88uu7zsYk
KVOYzejCLY1wIzJngpvBijLeoJPnA6GgHr/2X47s0i+0WshMaZoQZXodl9xii5JFxfQART
XIZU6kizmYhIw6DaWxb9eik9bBOgqlvA48V0JEJPSSuuYSx/gcXljDQhCVWgpmuF9OGiZg
u8yQIuchnnk9EilajKIWrxfdDul4mBQkB7sFzz8h36HKoLnZl56M8/ug+msSSmTMELDad1
irpjo5c1p8QQggdZdozekWoPGidtu+4jBV0cI0y//I2hLFuCpMrA39Bo1dv4qm69KuW9Gq
w2G7+wqxgUfrh9MBs/2aINceYmQDTm4WVC6Q6y1SYnRDHNux1wW+zzLDQ8uwRmEQdcnNHd
MvN6fJlKrzpVkgK8Z9VpwGTtOKQIE63Ja4RIeGVzpJ9kp/Nnrtw6UPIp5KJWOCH54SH2zi
HnpqFDatbTlKroaMPRi0TOiDYnhywQAAAAMBAAEAAAGAYycx4oEhp55Zz1HijblxnsEmQ8
kbbH1pV04fdm7HTxFis0Qu8PVIp5JxNFiWWunnQ1Z5MgI23G9WT+XST4+RpwXBCLWGv9xu
UsGOPpqUC/FdUiZf9MXBIxYgRjJS3xORA1KzsnAQ2sclb2I+B1pEl4d9yQWJesvQ25xa2H
Utzej/LgWkrk/ogSGRl6ZNImj/421wc0DouGyP+gUgtATt0/jT3LrlmAqUVCXVqssLYH2O
r9JTuGUibBJEW2W/c0lsM0jaHa5bGAdL3nhDuF1Q6KFB87mZoNw8c2znYoTzQ3FyWtIEZI
V/9oWrkS7V6242SKSR9tJoEzK0jtrKC/FZwBiI4hPcwoqY6fZbT1701i/n50xWEfEUOLVm
d6VqNKyAbIaZIPN0qfZuD+xdrHuM3V6k/rgFxGl4XTrp/N4AsruiQs0nRQKNTw3fHE0zPq
UTxSeMvjywRCepxhBFCNh8NHydapclHtEPEGdTVHohL3krJehstPO/IuRyKLfSVtL1AAAA
wQCmGA8k+uW6mway9J3jp8mlMhhp3DCX6DAcvalbA/S5OcqMyiTM3c/HD5OJ6OYFDldcqu
MPEgLRL2HfxL29LsbQSzjyOIrfp5PLJlo70P5lXS8u2QPbo4/KQJmQmsIX18LDyU2zRtNA
C2WfBiHSZV+guLhmHms9S5gQYKt2T5OnY/W0tmnInx9lmFCMC+XKS1iSQ2o433IrtCPQJp
IXZd59OQpO9QjJABgJIDtXxFIXt45qpXduDPJuggrhg81stOwAAADBAPX73u/CY+QUPts+
LV185Z4mZ2y+qu2ZMCAU3BnpHktGZZ1vFN1Xq9o8KdnuPZ+QJRdO8eKMWpySqrIdIbTYLm
9nXmVH0uNECIEAvdU+wgKeR+BSHxCRVuTF4YSygmNadgH/z+oRWLgOblGo2ywFBoXsIAKQ
paNu1MFGRUmhz67+dcpkkBUDRU9loAgBKexMo8D9vkR0YiHLOUjCrtmEZRNm0YRZt0gQhD
ZSD1fOH0fZDcCVNpGP2zqAKos4EGLnkwAAAMEAy/AuLtPKA2u9oCA8e18ZnuQRAi27FBVU
rU2D7bMg1eS0IakG8v0gE9K6WdYzyArY1RoKB3ZklK5VmJ1cOcWc2x3Ejc5jcJgc8cC6lZ
wwjpE8HfWL1kIIYgPdcexqFc+l6MdgH6QMKU3nLg1LsM4v5FEldtk/2dmnw620xnFfstpF
VxSZNdKrYfM/v9o6sRaDRqSfH1dG8BvkUxPznTAF+JDxBENcKXYECcq9f6dcl1w5IEnNTD
Wry/EKQvgvOUjbAAAAFG90aGVyLXRlc3RAcmVrb3IuZGV2AQIDBAUG
-----END OPENSSH PRIVATE KEY-----
`
otherSSHPublicKey = `ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDD9YJJYL1MS8JA7A75PrwS81rc5Ii/VjT6xPy5K/KM/IQ7T6nS3wKD/PLru87GJClTmM3owi2NcCMyZ4KbwYoy3qCT5wOhoB6/9l+O7NIvtFrITGmaEGV6HZfcYouSRcX0AEU1yGVOpIs5mISMOg2lsW/XopPWwToKpbwOPFdCRCT0krrmEsf4HF5Yw0IQlVoKZrhfThomYLvMkCLnIZ55PRIpWoyiFq8X3Q7peJgUJAe7Bc8/Id+hyqC52ZeejPP7oPprEkpkzBCw2ndYq6Y6OXNafEEIIHWXaM3pFqDxonbbvuIwVdHCNMv/yNoSxbgqTKwN/QaNXb+KpuvSrlvRqsNhu/sKsYFH64fTAbP9miDXHmJkA05uFlQukOstUmJ0QxzbsdcFvs8yw0PLsEZhEHXJzR3TLzenyZSq86VZICvGfVacBk7TikCBOtyWuESHhlc6SfZKfzZ67cOlDyKeSiVjgh+eEh9s4h56ahQ2rW05Sq6GjD0YtEzog2J4csE= other-test@rekor.dev
`
// Generated with ssh-keygen -C test@rekor.dev -t ed25519 -f id_ed25519
ed25519PrivateKey = `-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACBB45zRHxPPFtabwS3Vd6Lb9vMe+tIHZj2qN5VQ+bgLfQAAAJgyRa3cMkWt
3AAAAAtzc2gtZWQyNTUxOQAAACBB45zRHxPPFtabwS3Vd6Lb9vMe+tIHZj2qN5VQ+bgLfQ
AAAED7y4N/DsVnRQiBZNxEWdsJ9RmbranvtQ3X9jnb6gFed0HjnNEfE88W1pvBLdV3otv2
8x760gdmPao3lVD5uAt9AAAADnRlc3RAcmVrb3IuZGV2AQIDBAUGBw==
-----END OPENSSH PRIVATE KEY-----
`
ed25519PublicKey = `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEHjnNEfE88W1pvBLdV3otv28x760gdmPao3lVD5uAt9 test@rekor.dev
`
)
func TestFromOpenSSH(t *testing.T) {
for _, tt := range []struct {
name string
pub string
priv string
}{
{
name: "rsa",
pub: sshPublicKey,
priv: sshPrivateKey,
},
{
name: "ed25519",
pub: ed25519PublicKey,
priv: ed25519PrivateKey,
},
} {
if _, err := exec.LookPath("ssh-keygen"); err != nil {
t.Skip("skip TestFromOpenSSH: missing ssh-keygen in PATH")
}
t.Run(tt.name, func(t *testing.T) {
tt := tt
// Test that a signature from the cli can validate here.
td := t.TempDir()
data := []byte("hello, ssh world")
dataPath := write(t, []byte(data), td, "data")
privPath := write(t, []byte(tt.priv), td, "id")
write(t, []byte(tt.pub), td, "id.pub")
sigPath := dataPath + ".sig"
run(t, nil, "ssh-keygen", "-Y", "sign", "-n", "file", "-f", privPath, dataPath)
sigBytes, err := ioutil.ReadFile(sigPath)
if err != nil {
t.Fatal(err)
}
if err := Verify(bytes.NewReader(data), sigBytes, []byte(tt.pub), "file"); err != nil {
t.Error(err)
}
// It should not verify if we check against another public key
if err := Verify(bytes.NewReader(data), sigBytes, []byte(otherSSHPublicKey), "file"); err == nil {
t.Error("expected error with incorrect key")
}
// It should not verify if the data is tampered
if err := Verify(strings.NewReader("bad data"), sigBytes, []byte(sshPublicKey), "file"); err == nil {
t.Error("expected error with incorrect data")
}
})
}
}
func TestToOpenSSH(t *testing.T) {
for _, tt := range []struct {
name string
pub string
priv string
}{
{
name: "rsa",
pub: sshPublicKey,
priv: sshPrivateKey,
},
{
name: "ed25519",
pub: ed25519PublicKey,
priv: ed25519PrivateKey,
},
} {
if _, err := exec.LookPath("ssh-keygen"); err != nil {
t.Skip("skip TestToOpenSSH: missing ssh-keygen in PATH")
}
t.Run(tt.name, func(t *testing.T) {
tt := tt
// Test that a signature from here can validate in the CLI.
td := t.TempDir()
data := []byte("hello, ssh world")
write(t, []byte(data), td, "data")
armored, err := Sign([]byte(tt.priv), bytes.NewReader(data), "file")
if err != nil {
t.Fatal(err)
}
sigPath := write(t, []byte(armored), td, "oursig")
// Create an allowed_signers file with two keys to check against.
allowedSigner := "test@rekor.dev " + tt.pub + "\n"
allowedSigner += "othertest@rekor.dev " + otherSSHPublicKey + "\n"
allowedSigners := write(t, []byte(allowedSigner), td, "allowed_signer")
// We use the correct principal here so it should work.
run(t, data, "ssh-keygen", "-Y", "verify", "-f", allowedSigners,
"-I", "test@rekor.dev", "-n", "file", "-s", sigPath)
// Just to be sure, check against the other public key as well.
runErr(t, data, "ssh-keygen", "-Y", "verify", "-f", allowedSigners,
"-I", "othertest@rekor.dev", "-n", "file", "-s", sigPath)
// It should error if we run it against other data
data = []byte("other data!")
runErr(t, data, "ssh-keygen", "-Y", "check-novalidate", "-n", "file", "-s", sigPath)
})
}
}
func TestRoundTrip(t *testing.T) {
data := []byte("my good data to be signed!")
// Create one extra signature for all the tests.
otherSig, err := Sign([]byte(otherSSHPrivateKey), bytes.NewReader(data), "file")
if err != nil {
t.Fatal(err)
}
for _, tt := range []struct {
name string
pub string
priv string
}{
{
name: "rsa",
pub: sshPublicKey,
priv: sshPrivateKey,
},
{
name: "ed25519",
pub: ed25519PublicKey,
priv: ed25519PrivateKey,
},
} {
t.Run(tt.name, func(t *testing.T) {
tt := tt
sig, err := Sign([]byte(tt.priv), bytes.NewReader(data), "file")
if err != nil {
t.Fatal(err)
}
// Check the signature against that data and public key
if err := Verify(bytes.NewReader(data), sig, []byte(tt.pub), "file"); err != nil {
t.Error(err)
}
// Now check it against invalid data.
if err := Verify(strings.NewReader("invalid data!"), sig, []byte(tt.pub), "file"); err == nil {
t.Error("expected error!")
}
// Now check it against the wrong key.
if err := Verify(bytes.NewReader(data), sig, []byte(otherSSHPublicKey), "file"); err == nil {
t.Error("expected error!")
}
// Now check it against an invalid signature data.
if err := Verify(bytes.NewReader(data), []byte("invalid signature!"), []byte(tt.pub), "file"); err == nil {
t.Error("expected error!")
}
// Once more, use the wrong signature and check it against the original (wrong public key)
if err := Verify(bytes.NewReader(data), otherSig, []byte(tt.pub), "file"); err == nil {
t.Error("expected error!")
}
// It should work against the correct public key.
if err := Verify(bytes.NewReader(data), otherSig, []byte(otherSSHPublicKey), "file"); err != nil {
t.Error(err)
}
})
}
}
func write(t *testing.T, d []byte, fp ...string) string {
p := filepath.Join(fp...)
if err := ioutil.WriteFile(p, d, 0o600); err != nil {
t.Fatal(err)
}
return p
}
func run(t *testing.T, stdin []byte, args ...string) {
t.Helper()
/* #nosec */
cmd := exec.Command(args[0], args[1:]...)
cmd.Stdin = bytes.NewReader(stdin)
out, err := cmd.CombinedOutput()
t.Logf("cmd %v: %s", cmd, string(out))
if err != nil {
t.Fatal(err)
}
}
func runErr(t *testing.T, stdin []byte, args ...string) {
t.Helper()
/* #nosec */
cmd := exec.Command(args[0], args[1:]...)
cmd.Stdin = bytes.NewReader(stdin)
out, err := cmd.CombinedOutput()
t.Logf("cmd %v: %s", cmd, string(out))
if err == nil {
t.Fatal("expected error")
}
}

29
sshsig.go Normal file
View file

@ -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"

1
testdata/hello_world.txt vendored Normal file
View file

@ -0,0 +1 @@
Hello, ssh world!

19
testdata/hello_world.txt.sig vendored Normal file
View file

@ -0,0 +1,19 @@
-----BEGIN SSH SIGNATURE-----
U1NIU0lHAAAAAQAAAZcAAAAHc3NoLXJzYQAAAAMBAAEAAAGBAM2JmobMfIz+/RFT8RWbF4
x69qm8kgPr9ly8cGLGMDzJn4WbTyk141VlXRdogpPWEj78nHsmhijgp14rfJ/tvgwCjl3B
MkvTPQfCH5M0tlbpy2LUdo1JP79KOAyssKob93iGPPBnOvg8YlX3HhGi6XZuXovN9m1CG7
mons0VYyeaL7/1Vj4iQGBkXH41jVG+rUZC5xYY6lPMzgxHmsljzRiX99rQoAZOwPjVk8Hu
Db7qNkjVlAaZ7kftG7/Q40NqLchsw1Q/OYlQqrdSb+rp43YTOdPpR9eOUbasOkG32/GDz7
ylS7Q7+7OltRNTakOkFSc/h91h1hcf7/YDreOUp9UKTX7EGdCmjArnHaXnKuZRZ6GgtGsZ
aeOKJtgW6Xcaff8ydY4ZdJvQTzlhMPoXuOCKoyDkbKmbkwEAn8D1n9lMUsNmANZsCGi9al
jQbxaWbFQ/HeUgiU5U813GwHSe5NPDqV3zYtVqEJqWMGIOhUriYIgp+wayFvx3DIXp7kEG
awAAAARmaWxlAAAAAAAAAAZzaGE1MTIAAAGUAAAADHJzYS1zaGEyLTUxMgAAAYCKb6hW7f
e+oN2hTje0ymbQbm6RCxvUID+qxA3uXpGaHXXOh2JDkjqTZE0aCbFIhrjLKsl5L4eu2Upn
OmJf1og4Wi4qpMaFu1QS5j5OMzimJl/60iP0GklpQvfj/BaP2S2NoelTiHvZcbOUxtwVwX
/ya5+MdB21mriZDXL8krEnx9XqHMga6ReCUJ1wN/IeA30xWg9k7acoR9q/UfaTauwbEa67
upCfAWLSlQxfTd5LZpw2Re695vXRZnKT3Xf7vXVPhLSLAZBn2zHm+Mk++oy5VGAxjf8+o1
yYSGv6xpiBynenZv50WffLyaZ1X1YtZ/WBiiz7lSdeulZAR3iDSWJYUDo7kREb68trygjV
a68kEamlIMCDOo2wbWk8Z+yoYyT6qp9EbiWBuqexFonAnvabxOp3kQ4bGDeXkcjIyauhtZ
Y92S+qadtwmi+4cLzo/yVEvIeNsA7NOGSpVI+fdOW45R8E1nQLnf8LczXRESVgv//9KMtx
QM1YRBRG6BaBhg8=
-----END SSH SIGNATURE-----

38
testdata/id_rsa vendored Normal file
View file

@ -0,0 +1,38 @@
-----BEGIN OPENSSH PRIVATE KEY-----
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
-----END OPENSSH PRIVATE KEY-----

1
testdata/id_rsa.pub vendored Normal file
View file

@ -0,0 +1 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDNiZqGzHyM/v0RU/EVmxeMevapvJID6/ZcvHBixjA8yZ+Fm08pNeNVZV0XaIKT1hI+/Jx7JoYo4KdeK3yf7b4MAo5dwTJL0z0Hwh+TNLZW6cti1HaNST+/SjgMrLCqG/d4hjzwZzr4PGJV9x4Roul2bl6LzfZtQhu5qJ7NFWMnmi+/9VY+IkBgZFx+NY1Rvq1GQucWGOpTzM4MR5rJY80Yl/fa0KAGTsD41ZPB7g2+6jZI1ZQGme5H7Ru/0ONDai3IbMNUPzmJUKq3Um/q6eN2EznT6UfXjlG2rDpBt9vxg8+8pUu0O/uzpbUTU2pDpBUnP4fdYdYXH+/2A63jlKfVCk1+xBnQpowK5x2l5yrmUWehoLRrGWnjiibYFul3Gn3/MnWOGXSb0E85YTD6F7jgiqMg5Gypm5MBAJ/A9Z/ZTFLDZgDWbAhovWpY0G8WlmxUPx3lIIlOVPNdxsB0nuTTw6ld82LVahCaljBiDoVK4mCIKfsGshb8dwyF6e5BBms= test@rekor.dev

56
verify.go Normal file
View 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)
}