Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

crypto: signing builds with signify/minisign #21798

Merged
merged 25 commits into from
Nov 27, 2020
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
419369d
internal/build: implement signify's signing func
gballet Nov 6, 2020
20331e9
Add signify to the ci utility
gballet Nov 8, 2020
ea463a2
fix output file format
gballet Nov 8, 2020
0028e1d
Add unit test for signify
gballet Nov 8, 2020
d875837
holiman's + travis' feedback
gballet Nov 16, 2020
9fb07a8
internal/build: verify signify's output
gballet Nov 17, 2020
c4fee15
crypto: move signify to common dir
gballet Nov 17, 2020
5eb3540
use go-minisign to verify binaries
gballet Nov 17, 2020
80d45d0
more holiman feedback
gballet Nov 18, 2020
3e1f47c
crypto, ci: support minisign output
gballet Nov 18, 2020
f035c15
only accept one-line trusted comments
gballet Nov 18, 2020
80c928c
configurable untrusted comments
gballet Nov 18, 2020
951d75c
code cleanup in tests
gballet Nov 18, 2020
68f8638
revert to use ed25519 from the stdlib
gballet Nov 20, 2020
6726a72
bug: fix for empty untrusted comments
gballet Nov 23, 2020
76886da
write timestamp as comment if trusted comment isn't present
gballet Nov 23, 2020
3fe9eb6
rename line checker to commentHasManyLines
gballet Nov 23, 2020
dfac93a
crypto: added signify fuzzer (#6)
MariusVanDerWijden Nov 23, 2020
b2cf7c6
extract key data inside a single function
gballet Nov 23, 2020
76db596
don't treat \r as a newline
gballet Nov 23, 2020
f92a450
travis: fix signing command line
gballet Nov 24, 2020
6fa7fc6
do not use an external binary in tests
gballet Nov 24, 2020
40b8b5f
crypto: move signify to crypto/signify
gballet Nov 24, 2020
698af71
travis: fix formatting issue
gballet Nov 27, 2020
b22cdb2
ci: fix linter build after package move
gballet Nov 27, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ jobs:
- fakeroot
- python-bzrlib
- python-paramiko
- signify-openbsd
script:
- echo '|1|7SiYPr9xl3uctzovOTj4gMwAC1M=|t6ReES75Bo/PxlOPJ6/GsGbTrM0= ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA0aKz5UTUndYgIGG7dQBV+HaeuEZJ2xPHo2DS2iSKvUL4xNMSAY4UguNW+pX56nAQmZKIZZ8MaEvSj6zMEDiq6HFfn5JcTlM80UwlnyKe8B8p7Nk06PPQLrnmQt5fh0HmEcZx+JU9TZsfCHPnX7MNz4ELfZE6cFsclClrKim3BHUIGq//t93DllB+h4O9LHjEUsQ1Sr63irDLSutkLJD6RXchjROXkNirlcNVHH/jwLWR5RcYilNX7S5bIkK8NlWPjsn/8Ua5O7I9/YoE97PpO6i73DTGLh5H9JN/SITwCKBkgSDWUt61uPK3Y11Gty7o2lWsBjhBUm2Y38CBsoGmBw==' >> ~/.ssh/known_hosts
- go run build/ci.go debsrc -upload ethereum/ethereum -sftp-user geth-ci -signer "Go Ethereum Linux Builder <geth-ci@ethereum.org>"
Expand All @@ -64,25 +65,32 @@ jobs:
apt:
packages:
- gcc-multilib
- signify-openbsd
script:
# Build for the primary platforms that Trusty can manage
- go run build/ci.go install -dlgo
- go run build/ci.go archive -type tar -signer LINUX_SIGNING_KEY -upload gethstore/builds
- go run build/ci.go archive -type tar -signer LINUX_SIGNIFY_KEY -upload gethstore/builds
- go run build/ci.go install -dlgo -arch 386
- go run build/ci.go archive -arch 386 -type tar -signer LINUX_SIGNING_KEY -upload gethstore/builds
- go run build/ci.go archive -arch 386 -type tar -signer LINUX_SIGNIFY_KEY -upload gethstore/builds

# Switch over GCC to cross compilation (breaks 386, hence why do it here only)
- sudo -E apt-get -yq --no-install-suggests --no-install-recommends --force-yes install gcc-arm-linux-gnueabi libc6-dev-armel-cross gcc-arm-linux-gnueabihf libc6-dev-armhf-cross gcc-aarch64-linux-gnu libc6-dev-arm64-cross
- sudo ln -s /usr/include/asm-generic /usr/include/asm

- GOARM=5 go run build/ci.go install -dlgo -arch arm -cc arm-linux-gnueabi-gcc
- GOARM=5 go run build/ci.go archive -arch arm -type tar -signer LINUX_SIGNING_KEY -upload gethstore/builds
- GOARM=5 go run build/ci.go archive -arch arm -type tar -signer LINUX_SIGNIFY_KEY -upload gethstore/builds
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please fix those lines up.

- GOARM=6 go run build/ci.go install -dlgo -arch arm -cc arm-linux-gnueabi-gcc
- GOARM=6 go run build/ci.go archive -arch arm -type tar -signer LINUX_SIGNING_KEY -upload gethstore/builds
- GOARM=6 go run build/ci.go archive -arch arm -type tar -signer LINUX_SIGNIFY_KEY -upload gethstore/builds
- GOARM=7 go run build/ci.go install -dlgo -arch arm -cc arm-linux-gnueabihf-gcc
- GOARM=7 go run build/ci.go archive -arch arm -type tar -signer LINUX_SIGNING_KEY -upload gethstore/builds
- GOARM=7 go run build/ci.go archive -arch arm -type tar -signer LINUX_SIGNIFY_KEY -upload gethstore/builds
- go run build/ci.go install -dlgo -arch arm64 -cc aarch64-linux-gnu-gcc
- go run build/ci.go archive -arch arm64 -type tar -signer LINUX_SIGNING_KEY -upload gethstore/builds
- go run build/ci.go archive -arch arm64 -type tar -signer LINUX_SIGNIFY_KEY -upload gethstore/builds

# This builder does the Linux Azure MIPS xgo uploads
- stage: build
Expand All @@ -101,18 +109,22 @@ jobs:
- go run build/ci.go xgo --alltools -- --targets=linux/mips --ldflags '-extldflags "-static"' -v
- for bin in build/bin/*-linux-mips; do mv -f "${bin}" "${bin/-linux-mips/}"; done
- go run build/ci.go archive -arch mips -type tar -signer LINUX_SIGNING_KEY -upload gethstore/builds
- go run build/ci.go archive -arch mips -type tar -signer LINUX_SIGNIFY_KEY -upload gethstore/builds

- go run build/ci.go xgo --alltools -- --targets=linux/mipsle --ldflags '-extldflags "-static"' -v
- for bin in build/bin/*-linux-mipsle; do mv -f "${bin}" "${bin/-linux-mipsle/}"; done
- go run build/ci.go archive -arch mipsle -type tar -signer LINUX_SIGNING_KEY -upload gethstore/builds
- go run build/ci.go archive -arch mipsle -type tar -signer LINUX_SIGNIFY_KEY -upload gethstore/builds

- go run build/ci.go xgo --alltools -- --targets=linux/mips64 --ldflags '-extldflags "-static"' -v
- for bin in build/bin/*-linux-mips64; do mv -f "${bin}" "${bin/-linux-mips64/}"; done
- go run build/ci.go archive -arch mips64 -type tar -signer LINUX_SIGNING_KEY -upload gethstore/builds
- go run build/ci.go archive -arch mips64 -type tar -signer LINUX_SIGNIFY_KEY -upload gethstore/builds

- go run build/ci.go xgo --alltools -- --targets=linux/mips64le --ldflags '-extldflags "-static"' -v
- for bin in build/bin/*-linux-mips64le; do mv -f "${bin}" "${bin/-linux-mips64le/}"; done
- go run build/ci.go archive -arch mips64le -type tar -signer LINUX_SIGNING_KEY -upload gethstore/builds
- go run build/ci.go archive -arch mips64le -type tar -signer LINUX_SIGNIFY_KEY -upload gethstore/builds

# This builder does the Android Maven and Azure uploads
- stage: build
Expand Down Expand Up @@ -152,6 +164,7 @@ jobs:
- mkdir -p $GOPATH/src/github.com/ethereum
- ln -s `pwd` $GOPATH/src/github.com/ethereum/go-ethereum
- go run build/ci.go aar -signer ANDROID_SIGNING_KEY -deploy https://oss.sonatype.org -upload gethstore/builds
- go run build/ci.go aar -signer ANDROID_SIGNIFY_KEY -deploy https://oss.sonatype.org -upload gethstore/builds

# This builder does the OSX Azure, iOS CocoaPods and iOS Azure uploads
- stage: build
Expand All @@ -168,6 +181,7 @@ jobs:
script:
- go run build/ci.go install -dlgo
- go run build/ci.go archive -type tar -signer OSX_SIGNING_KEY -upload gethstore/builds
- go run build/ci.go archive -type tar -signer OSX_SIGNIFY_KEY -upload gethstore/builds

# Build the iOS framework and upload it to CocoaPods and Azure
- gem uninstall cocoapods -a -x
Expand All @@ -183,6 +197,7 @@ jobs:
# Workaround for https://github.com/golang/go/issues/23749
- export CGO_CFLAGS_ALLOW='-fmodules|-fblocks|-fobjc-arc'
- go run build/ci.go xcode -signer IOS_SIGNING_KEY -deploy trunk -upload gethstore/builds
- go run build/ci.go xcode -signer IOS_SIGNIFY_KEY -deploy trunk -upload gethstore/builds

# These builders run the tests
- stage: build
Expand Down
52 changes: 34 additions & 18 deletions build/ci.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import (
"time"

"github.com/cespare/cp"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/internal/build"
"github.com/ethereum/go-ethereum/params"
)
Expand Down Expand Up @@ -396,11 +397,12 @@ func downloadLinter(cachedir string) string {
// Release Packaging
func doArchive(cmdline []string) {
var (
arch = flag.String("arch", runtime.GOARCH, "Architecture cross packaging")
atype = flag.String("type", "zip", "Type of archive to write (zip|tar)")
signer = flag.String("signer", "", `Environment variable holding the signing key (e.g. LINUX_SIGNING_KEY)`)
upload = flag.String("upload", "", `Destination to upload the archives (usually "gethstore/builds")`)
ext string
arch = flag.String("arch", runtime.GOARCH, "Architecture cross packaging")
atype = flag.String("type", "zip", "Type of archive to write (zip|tar)")
signer = flag.String("signer", "", `Environment variable holding the signing key (e.g. LINUX_SIGNING_KEY)`)
signify = flag.String("signify", "", `Environment variable holding the signify key (e.g. LINUX_SIGNIFY_KEY)`)
upload = flag.String("upload", "", `Destination to upload the archives (usually "gethstore/builds")`)
ext string
)
flag.CommandLine.Parse(cmdline)
switch *atype {
Expand All @@ -427,7 +429,7 @@ func doArchive(cmdline []string) {
log.Fatal(err)
}
for _, archive := range []string{geth, alltools} {
if err := archiveUpload(archive, *upload, *signer); err != nil {
if err := archiveUpload(archive, *upload, *signer, *signify); err != nil {
log.Fatal(err)
}
}
Expand All @@ -447,14 +449,20 @@ func archiveBasename(arch string, archiveVersion string) string {
return platform + "-" + archiveVersion
}

func archiveUpload(archive string, blobstore string, signer string) error {
func archiveUpload(archive string, blobstore string, signer string, signify string) error {
// If signing was requested, generate the signature files
if signer != "" {
key := getenvBase64(signer)
if err := build.PGPSignFile(archive, archive+".asc", string(key)); err != nil {
return err
}
}
if signify != "" {
key := getenvBase64(string(signify))
if err := crypto.SignifySignFile(archive, archive+".sig", string(key), "verify with geth.pub", fmt.Sprintf("%d", time.Now().UTC().Unix())); err != nil {
return err
}
}
gballet marked this conversation as resolved.
Show resolved Hide resolved
// If uploading to Azure was requested, push the archive possibly with its signature
if blobstore != "" {
auth := build.AzureBlobstoreConfig{
Expand All @@ -470,6 +478,11 @@ func archiveUpload(archive string, blobstore string, signer string) error {
return err
}
}
if signify != "" {
if err := build.AzureBlobstoreUpload(archive+".sig", filepath.Base(archive+".sig"), auth); err != nil {
return err
}
}
}
return nil
}
Expand Down Expand Up @@ -806,6 +819,7 @@ func doWindowsInstaller(cmdline []string) {
var (
arch = flag.String("arch", runtime.GOARCH, "Architecture for cross build packaging")
signer = flag.String("signer", "", `Environment variable holding the signing key (e.g. WINDOWS_SIGNING_KEY)`)
signify = flag.String("signify key", "", `Environment variable holding the signify signing key (e.g. WINDOWS_SIGNIFY_KEY)`)
upload = flag.String("upload", "", `Destination to upload the archives (usually "gethstore/builds")`)
workdir = flag.String("workdir", "", `Output directory for packages (uses temp dir if unset)`)
)
Expand Down Expand Up @@ -867,7 +881,7 @@ func doWindowsInstaller(cmdline []string) {
filepath.Join(*workdir, "geth.nsi"),
)
// Sign and publish installer.
if err := archiveUpload(installer, *upload, *signer); err != nil {
if err := archiveUpload(installer, *upload, *signer, *signify); err != nil {
log.Fatal(err)
}
}
Expand All @@ -876,10 +890,11 @@ func doWindowsInstaller(cmdline []string) {

func doAndroidArchive(cmdline []string) {
var (
local = flag.Bool("local", false, `Flag whether we're only doing a local build (skip Maven artifacts)`)
signer = flag.String("signer", "", `Environment variable holding the signing key (e.g. ANDROID_SIGNING_KEY)`)
deploy = flag.String("deploy", "", `Destination to deploy the archive (usually "https://oss.sonatype.org")`)
upload = flag.String("upload", "", `Destination to upload the archive (usually "gethstore/builds")`)
local = flag.Bool("local", false, `Flag whether we're only doing a local build (skip Maven artifacts)`)
signer = flag.String("signer", "", `Environment variable holding the signing key (e.g. ANDROID_SIGNING_KEY)`)
signify = flag.String("signify", "", `Environment variable holding the signify signing key (e.g. ANDROID_SIGNIFY_KEY)`)
deploy = flag.String("deploy", "", `Destination to deploy the archive (usually "https://oss.sonatype.org")`)
upload = flag.String("upload", "", `Destination to upload the archive (usually "gethstore/builds")`)
)
flag.CommandLine.Parse(cmdline)
env := build.Env()
Expand Down Expand Up @@ -908,7 +923,7 @@ func doAndroidArchive(cmdline []string) {
archive := "geth-" + archiveBasename("android", params.ArchiveVersion(env.Commit)) + ".aar"
os.Rename("geth.aar", archive)

if err := archiveUpload(archive, *upload, *signer); err != nil {
if err := archiveUpload(archive, *upload, *signer, *signify); err != nil {
log.Fatal(err)
}
// Sign and upload all the artifacts to Maven Central
Expand Down Expand Up @@ -1001,10 +1016,11 @@ func newMavenMetadata(env build.Environment) mavenMetadata {

func doXCodeFramework(cmdline []string) {
var (
local = flag.Bool("local", false, `Flag whether we're only doing a local build (skip Maven artifacts)`)
signer = flag.String("signer", "", `Environment variable holding the signing key (e.g. IOS_SIGNING_KEY)`)
deploy = flag.String("deploy", "", `Destination to deploy the archive (usually "trunk")`)
upload = flag.String("upload", "", `Destination to upload the archives (usually "gethstore/builds")`)
local = flag.Bool("local", false, `Flag whether we're only doing a local build (skip Maven artifacts)`)
signer = flag.String("signer", "", `Environment variable holding the signing key (e.g. IOS_SIGNING_KEY)`)
signify = flag.String("signify", "", `Environment variable holding the signify signing key (e.g. IOS_SIGNIFY_KEY)`)
deploy = flag.String("deploy", "", `Destination to deploy the archive (usually "trunk")`)
upload = flag.String("upload", "", `Destination to upload the archives (usually "gethstore/builds")`)
)
flag.CommandLine.Parse(cmdline)
env := build.Env()
Expand Down Expand Up @@ -1032,7 +1048,7 @@ func doXCodeFramework(cmdline []string) {
maybeSkipArchive(env)

// Sign and upload the framework to Azure
if err := archiveUpload(archive+".tar.gz", *upload, *signer); err != nil {
if err := archiveUpload(archive+".tar.gz", *upload, *signer, *signify); err != nil {
log.Fatal(err)
}
// Prepare and upload a PodSpec to CocoaPods
Expand Down
115 changes: 115 additions & 0 deletions crypto/signify.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// Copyright 2020 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.

// signFile reads the contents of an input file and signs it (in armored format)
// with the key provided, placing the signature into the output file.

package crypto

import (
"encoding/base64"
"errors"
"fmt"
"io/ioutil"
"os"
"strings"

"crypto/ed25519"
)

var (
errInvalidKeyHeader = errors.New("Incorrect key header")
errInvalidKeyLength = errors.New("invalid, key length != 104")
)

func readSKey(key []byte) (ed25519.PrivateKey, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe you could turn this into parsePrivateKey(pKey []byte) ((ed25519.PrivateKey, hdr []byte, keyId []byte, error). Then the key format wouldn't be as "spread out", and you wouldn't keep working on the keydata buffer (which contains secret data) after calling this method.

if len(key) != 104 {
return nil, errInvalidKeyLength
}

if string(key[:2]) != "Ed" {
return nil, errInvalidKeyHeader
}

return ed25519.PrivateKey(key[40:]), nil

}

func isCommentOnlyOneLine(comment string) bool {
firstCRIndex := strings.IndexByte(comment, 13)
firstLFIndex := strings.IndexByte(comment, 10)
return (firstCRIndex >= 0 && firstCRIndex < len(comment)-1) || (firstLFIndex >= 0 && firstLFIndex < len(comment)-1)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Afaict, this allows arbitrarily many CR or LF characters to be placed anywhere?
A one-liner to me means that it contains exactly one or zero CR or CRLF, and that is placed at the end.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the code is correct, it's the function name that is wrong. It should be commentMoreThanOneLine. If this returns true, then an error will be returned.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The go-minisign verifier does this:

	lines := strings.SplitN(in, "\n", 4)

So it totally ignores CR and counts only LF.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it's not true in Go, but \n is OS-dependent in C, so it could be CR, LF, or both (don't have Windows/MacOS, can't check that). Maybe @fjl can chime in here? I could remove the check for LF but I want to make sure that it's going to work on Windows/MacOS.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On unix-systems, \n (LF) is used, whereas windows uses \r\n (CRLF). We should we stick to whatever signify and_or minisign does, and hopefully both of them behaves the same way.

If they allow "lalalal \r lolo \r foo bar\n" then we should do that too

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On unix-systems, \n (LF) is used, whereas windows uses \r\n (CRLF). We should we stick to whatever signify and_or minisign does, and hopefully both of them behaves the same way.

If they allow "lalalal \r lolo \r foo bar\n" then we should do that too

this is what I said, and MacOS only uses CR in some modes. However, I guess go-minisign will behave the same on every platform, which means that it will expect CR only. I just tested signify-openbsd and it's ignoring LF. So I assume there is no need to test on Windows/MacOS after all.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh right, yes, a C program in windows will require \r\n, of course. My bad. Monday mornings. 😴 Well, I just pushed the code to do that anyway.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just tested signify-openbsd and it's ignoring LF. So I assume there is no need to test on Windows/MacOS after all.

Agreed

this is what I said

No, you said that \n is OS-depdendent, which is not true. It's very well defined :-p

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, you said that \n is OS-depdendent, which is not true. It's very well defined :-p

Someone doesn't refresh before posting :D

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Someone doesn't refresh before posting :D

Yup, I hate that -- sometimes GH just plops in whatever new content there is, and sometimes you have to manually reload pages. Solly

}

// SignifySignFile creates a signature of the input file.
func SignifySignFile(input string, output string, key string, unTrustedComment string, trustedComment string) error {
in, err := os.Open(input)
if err != nil {
return err
}
defer in.Close()

out, err := os.Create(output)
if err != nil {
return err
}
defer out.Close()

keydata, err := base64.StdEncoding.DecodeString(key)
if err != nil {
return err
}
skey, err := readSKey(keydata)
if err != nil {
return err
}

filedata, err := ioutil.ReadAll(in)
if err != nil {
return err
}

rawSig := ed25519.Sign(skey, filedata)
header := keydata[:2]
keyNum := keydata[32:40]

var sigdata []byte
sigdata = append(sigdata, header...)
sigdata = append(sigdata, keyNum...)
sigdata = append(sigdata, rawSig...)

// Check that the trusted comment fits in one line
if isCommentOnlyOneLine(unTrustedComment) {
return errors.New("untrusted comment must fit on a single line")
}

out.WriteString(fmt.Sprintf("untrusted comment: %s\n%s\n", unTrustedComment, base64.StdEncoding.EncodeToString(sigdata)))

// Add the trusted comment if available (minisign only)
if trustedComment != "" {
// Check that the trusted comment fits in one line
if isCommentOnlyOneLine(trustedComment) {
return errors.New("trusted comment must fit on a single line")
}

var sigAndComment []byte
sigAndComment = append(sigAndComment, rawSig...)
sigAndComment = append(sigAndComment, []byte(trustedComment)...)
out.WriteString(fmt.Sprintf("trusted comment: %s\n%s\n", trustedComment, base64.StdEncoding.EncodeToString(ed25519.Sign(skey, sigAndComment))))
gballet marked this conversation as resolved.
Show resolved Hide resolved
}

return nil
}
Loading