1
1
mirror of https://github.com/goreleaser/nfpm synced 2024-09-27 23:09:52 +02:00

feat: Package Signing (#224)

* feat: Add initial openpgp signing capability.

* refactor: Rename signatures to sigs and expose verify method.

* feat: Add debsigs support.

* test: Add debsigs acceptance test.

* feat: Add RSA signing capability.

* feat: Add RPM signature support.

* test: Add RPM signature acceptance test.

* test: Move acceptance test keys in dedicated folder.

* feat: Add APK signature support.

* test: Add APK signature acceptance test.

* feat: Expose deb signature type in config.

* fix: Fix typo and superfluous explicit error check

* fix: Fix password env extraction and add tests.

* fix: Redirect rpmpack to temporary goreleaser vendoring.

* fix: Catch missing maintainer email if no apk key name is set.

* refactor: Put signature info in a dedicated struct.

* doc: Add signing documentation.

* fix: Add trailing newlines to some files and remove unnecessary comment.

* fix: Change wrong references from SHA256 to SHA1 in the sign package.

* fix: Improve error wording when no passphrase was provided.

* fix: Remove another SHA256 reference.

* fix: Fix signature errors.

* test: Split up environment variable test.

* fix: Don't parse passphrase from YAML.

* deps: Update redirected rpmpack.

* fix: Export and use signature related error type.

* doc: Document ErrSigningFailure.

* test: Fix apk signature error test.

* test: Add rpm signature error test.
This commit is contained in:
Erik G 2020-09-17 14:18:44 +02:00 committed by GitHub
parent e3d6f8344d
commit 285a6bcaea
Signed by: GitHub
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 1402 additions and 29 deletions

View File

@ -251,6 +251,20 @@ func TestDebBreaks(t *testing.T) {
})
}
func TestSignatures(t *testing.T) {
for _, format := range formats {
format := format
t.Run("signed", func(t *testing.T) {
accept(t, acceptParms{
Name: fmt.Sprintf("signed_%s", format),
Conf: "signed.yaml",
Format: format,
Dockerfile: fmt.Sprintf("%s.signed.dockerfile", format),
})
})
}
}
type acceptParms struct {
Name string
Conf string

View File

@ -0,0 +1,7 @@
FROM alpine
ARG package
COPY keys/rsa_unprotected.pub /etc/apk/keys/john@example.com.rsa.pub
COPY ${package} /tmp/foo.apk
RUN apk verify /tmp/foo.apk | grep "/tmp/foo.apk: 0 - OK"
RUN apk add /tmp/foo.apk

View File

@ -0,0 +1,31 @@
FROM ubuntu
ARG package
COPY keys/pubkey.gpg /usr/share/debsig/keyrings/BC8ACDD415BD80B3/debsig.gpg
COPY ${package} /tmp/foo.deb
RUN apt update -y
RUN apt install -y debsig-verify
RUN mkdir -p /etc/debsig/policies/BC8ACDD415BD80B3
RUN echo '<?xml version="1.0"?>\n\
<!DOCTYPE Policy SYSTEM "https://www.debian.org/debsig/1.0/policy.dtd">\n\
<Policy xmlns="https://www.debian.org/debsig/1.0/">\n\
\n\
<Origin Name="test" id="BC8ACDD415BD80B3" Description="Test package"/>\n\
\n\
<Selection>\n\
<Required Type="origin" File="debsig.gpg" id="BC8ACDD415BD80B3"/>\n\
</Selection>\n\
\n\
<Verification MinOptional="0">\n\
<Required Type="origin" File="debsig.gpg" id="BC8ACDD415BD80B3"/>\n\
</Verification>\n\
</Policy>\n\
\n' >> /etc/debsig/policies/BC8ACDD415BD80B3/policy.pol
# manually check signature
RUN debsig-verify /tmp/foo.deb | grep "debsig: Verified package from 'Test package' (test)"
# clear dpkg config as it contains 'no-debsig', now every
# package that will be installed must be signed
RUN echo "" > /etc/dpkg/dpkg.cfg
RUN dpkg -i /tmp/foo.deb

37
acceptance/testdata/keys/pubkey.asc vendored Normal file
View File

@ -0,0 +1,37 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
CTI=
=arXz
-----END PGP PUBLIC KEY BLOCK-----

BIN
acceptance/testdata/keys/pubkey.gpg vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxWFb75R71bdD12OL9Ba/
Z9/61byXEMatIpliLZZKf2Ul937SIGLECGKvw3DIlQfjdv/u6RIXLQL7ybKKT7R0
V8yZK6yhGXnjarPzeoOjaQBqbzR8tXdJJdpy9vbfTVbFonzb/e8uSgGtoKPwDosL
6JjcRz9hLl2B/UaHPhIEoqO4n6tOBeqcTJwAYV5YrPA+J1Tb8BBpuXPttUFFvXh4
bqeB/uLfCorMDP/OnQ51waVb6zxjHgeY+R35X885rb6XHCTbKPveEeIFkC13TiO9
Td9d34lvNX+2z+hlS997Gb8n1uGTGek4iEX/k0baI/FUYWzQtIUK7okWJlgSZg7Q
4wIDAQAB
-----END PUBLIC KEY-----

View File

@ -0,0 +1,10 @@
FROM fedora
ARG package
COPY keys/pubkey.asc /tmp/pubkey.asc
COPY ${package} /tmp/foo.rpm
RUN rpm --import /tmp/pubkey.asc
RUN rpm -K /tmp/foo.rpm | grep ": digests signatures OK"
RUN rpm -K /tmp/foo.rpm -v | grep "RSA/SHA256 Signature, key ID 15bd80b3: OK"
RUN rpm -i /tmp/foo.rpm

19
acceptance/testdata/signed.yaml vendored Normal file
View File

@ -0,0 +1,19 @@
name: "foo"
arch: "amd64"
platform: "linux"
version: "v1.0.0"
maintainer: "John Doe <john@example.com>"
description: This package is sigend
vendor: "FooBarCorp"
homepage: "http://example.com"
files:
../testdata/fake: "/usr/local/bin/fake"
deb:
signature:
key_file: ../internal/sign/testdata/privkey_unprotected.asc
rpm:
signature:
key_file: ../internal/sign/testdata/privkey_unprotected.asc
apk:
signature:
key_file: ../internal/sign/testdata/rsa_unprotected.priv

View File

@ -20,7 +20,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
// Package apk (someday) implements nfpm.Packager providing .apk bindings.
// Package apk implements nfpm.Packager providing .apk bindings.
package apk
// Initial implementation from https://gist.github.com/tcurdt/512beaac7e9c12dcf5b6b7603b09d0d8
@ -30,12 +30,15 @@ import (
"bufio"
"bytes"
"compress/gzip"
"crypto/sha1" // nolint:gosec
"crypto/sha256"
"encoding/hex"
"fmt"
"hash"
"io"
"io/ioutil"
"log"
"net/mail"
"os"
"path/filepath"
"strings"
@ -45,6 +48,8 @@ import (
"github.com/goreleaser/nfpm/internal/files"
"github.com/goreleaser/nfpm/internal/sign"
"github.com/pkg/errors"
"github.com/goreleaser/nfpm"
@ -109,19 +114,29 @@ func (*Apk) Package(info *nfpm.Info, apk io.Writer) (err error) {
size := int64(0)
// create the data tgz
_, err = createData(&bufData, info, &size)
dataDigest, err := createData(&bufData, info, &size)
if err != nil {
return err
}
// create the control tgz
var bufControl bytes.Buffer
if _, err = createControl(&bufControl, info, size); err != nil {
controlDigest, err := createControl(&bufControl, info, size, dataDigest)
if err != nil {
return err
}
// combine
return combineToApk(apk, &bufData, &bufControl)
if info.APK.Signature.KeyFile == "" {
return combineToApk(apk, &bufControl, &bufData)
}
// create the signature tgz
var bufSignature bytes.Buffer
if err = createSignature(&bufSignature, info, controlDigest); err != nil {
return err
}
return combineToApk(apk, &bufSignature, &bufControl, &bufData)
}
type writerCounter struct {
@ -224,17 +239,64 @@ func createData(dataTgz io.Writer, info *nfpm.Info, sizep *int64) ([]byte, error
return dataDigest, nil
}
func createControl(controlTgz io.Writer, info *nfpm.Info, size int64) ([]byte, error) {
builderControl := createBuilderControl(info, size)
controlDigest, err := writeTgz(controlTgz, tarCut, builderControl, sha256.New())
func createControl(controlTgz io.Writer, info *nfpm.Info, size int64, dataDigest []byte) ([]byte, error) {
builderControl := createBuilderControl(info, size, dataDigest)
controlDigest, err := writeTgz(controlTgz, tarCut, builderControl, sha1.New()) // nolint:gosec
if err != nil {
return nil, err
}
return controlDigest, nil
}
func combineToApk(target io.Writer, dataTgz, controlTgz io.Reader) error {
for _, tgz := range []io.Reader{controlTgz, dataTgz} {
func createSignature(signatureTgz io.Writer, info *nfpm.Info, controlSHA1Digest []byte) error {
signatureBuilder := createSignatureBuilder(controlSHA1Digest, info)
// we don't actually need to produce a digest here, but writeTgz
// requires it so we just use SHA1 since it is already imported
_, err := writeTgz(signatureTgz, tarCut, signatureBuilder, sha1.New()) // nolint:gosec
if err != nil {
return &nfpm.ErrSigningFailure{Err: err}
}
return nil
}
func createSignatureBuilder(digest []byte, info *nfpm.Info) func(*tar.Writer) error {
return func(tw *tar.Writer) error {
signature, err := sign.RSASignSHA1Digest(digest,
info.APK.Signature.KeyFile, info.APK.Signature.KeyPassphrase)
if err != nil {
return err
}
// needs to exist on the machine during installation: /etc/apk/keys/<keyname>.rsa.pub
keyname := info.APK.Signature.KeyName
if keyname == "" {
addr, err := mail.ParseAddress(info.Maintainer)
if err != nil {
return errors.Wrap(err, "key name not set and unable to parse maintainer mail address")
} else if addr.Address == "" {
return errors.New("key name not set and maintainer mail address empty")
}
keyname = addr.Address + ".rsa.pub"
}
// In principle apk supports RSA signatures over SHA256/512 keys, but in
// practice verification works but installation segfaults. If this is
// fixed at some point we should also upgrade the hash. In this case,
// the file name will have to start with .SIGN.RSA256 or .SIGN.RSA512.
signHeader := &tar.Header{
Name: fmt.Sprintf(".SIGN.RSA.%s", keyname),
Mode: 0600,
Size: int64(len(signature)),
}
return writeFile(tw, signHeader, bytes.NewReader(signature))
}
}
func combineToApk(target io.Writer, readers ...io.Reader) error {
for _, tgz := range readers {
if _, err := io.Copy(target, tgz); err != nil {
return err
}
@ -242,12 +304,13 @@ func combineToApk(target io.Writer, dataTgz, controlTgz io.Reader) error {
return nil
}
func createBuilderControl(info *nfpm.Info, size int64) func(tw *tar.Writer) error {
func createBuilderControl(info *nfpm.Info, size int64, dataDigest []byte) func(tw *tar.Writer) error {
return func(tw *tar.Writer) error {
var infoBuf bytes.Buffer
if err := writeControl(&infoBuf, controlData{
Info: info,
InstalledSize: size,
Datahash: hex.EncodeToString(dataDigest),
}); err != nil {
return err
}
@ -493,11 +556,13 @@ depend = {{ $dep }}
{{- if .Info.License}}
license = {{.Info.License}}
{{- end }}
datahash = {{.Datahash}}
`
type controlData struct {
Info *nfpm.Info
InstalledSize int64
Datahash string
}
func writeControl(w io.Writer, data controlData) error {

View File

@ -3,6 +3,9 @@ package apk
import (
"archive/tar"
"bytes"
"crypto/sha1" // nolint:gosec
"crypto/sha256"
"errors"
"flag"
"fmt"
"io"
@ -12,6 +15,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/goreleaser/nfpm"
"github.com/goreleaser/nfpm/internal/sign"
"github.com/stretchr/testify/assert"
)
@ -202,13 +206,13 @@ func TestNoFiles(t *testing.T) {
func TestCreateBuilderControl(t *testing.T) {
info := exampleInfo()
size := int64(12345)
builderControl := createBuilderControl(info, size)
builderControl := createBuilderControl(info, size, sha256.New().Sum(nil))
var w bytes.Buffer
tw := tar.NewWriter(&w)
assert.NoError(t, builderControl(tw))
var control = extractControl(t, &w)
var control = string(extractFromTar(t, w.Bytes(), ".PKGINFO"))
var golden = "testdata/TestCreateBuilderControl.golden"
if *update {
require.NoError(t, ioutil.WriteFile(golden, []byte(control), 0655)) // nolint: gosec
@ -228,13 +232,13 @@ func TestCreateBuilderControlScripts(t *testing.T) {
}
size := int64(12345)
builderControl := createBuilderControl(info, size)
builderControl := createBuilderControl(info, size, sha256.New().Sum(nil))
var w bytes.Buffer
tw := tar.NewWriter(&w)
assert.NoError(t, builderControl(tw))
var control = extractControl(t, &w)
var control = string(extractFromTar(t, w.Bytes(), ".PKGINFO"))
var golden = "testdata/TestCreateBuilderControlScripts.golden"
if *update {
require.NoError(t, ioutil.WriteFile(golden, []byte(control), 0655)) // nolint: gosec
@ -259,8 +263,51 @@ func TestControl(t *testing.T) {
assert.Equal(t, string(bts), w.String())
}
func extractControl(t *testing.T, r io.Reader) string {
var tr = tar.NewReader(r)
func TestSignature(t *testing.T) {
info := exampleInfo()
info.APK.Signature.KeyFile = "../internal/sign/testdata/rsa.priv"
info.APK.Signature.KeyName = "testkey.rsa.pub"
info.APK.Signature.KeyPassphrase = "hunter2"
digest := sha1.New().Sum(nil) // nolint:gosec
var signatureTarGz bytes.Buffer
tw := tar.NewWriter(&signatureTarGz)
require.NoError(t, createSignatureBuilder(digest, info)(tw))
signature := extractFromTar(t, signatureTarGz.Bytes(), ".SIGN.RSA.testkey.rsa.pub")
err := sign.RSAVerifySHA1Digest(digest, signature, "../internal/sign/testdata/rsa.pub")
require.NoError(t, err)
err = Default.Package(info, ioutil.Discard)
require.NoError(t, err)
}
func TestSignatureError(t *testing.T) {
info := exampleInfo()
info.APK.Signature.KeyFile = "../internal/sign/testdata/rsa.priv"
info.APK.Signature.KeyName = "testkey.rsa.pub"
info.APK.Signature.KeyPassphrase = "hunter2"
// wrong hash format
digest := sha256.New().Sum(nil)
var signatureTarGz bytes.Buffer
err := createSignature(&signatureTarGz, info, digest)
require.Error(t, err)
var expectedError *nfpm.ErrSigningFailure
require.True(t, errors.As(err, &expectedError))
_, ok := err.(*nfpm.ErrSigningFailure)
assert.True(t, ok)
}
func extractFromTar(t *testing.T, tarFile []byte, fileName string) []byte {
t.Helper()
var tr = tar.NewReader(bytes.NewReader(tarFile))
for {
hdr, err := tr.Next()
@ -268,14 +315,16 @@ func extractControl(t *testing.T, r io.Reader) string {
break
}
require.NoError(t, err)
if hdr.Name == ".PKGINFO" {
var w bytes.Buffer
_, err := io.Copy(&w, tr)
require.NoError(t, err)
return w.String()
if hdr.Name != fileName {
continue
}
t.Log("ignored", hdr.Name)
data, err := ioutil.ReadAll(tr)
require.NoError(t, err)
return data
}
return ""
t.Fatalf("file %q not found in tar file", fileName)
return nil
}

View File

@ -11,3 +11,4 @@ provides = bzr
provides = zzz
depend = bash
depend = foo
datahash =

View File

@ -11,3 +11,4 @@ provides = bzr
provides = zzz
depend = bash
depend = foo
datahash = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

View File

@ -11,3 +11,4 @@ provides = bzr
provides = zzz
depend = bash
depend = foo
datahash = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

View File

@ -19,6 +19,7 @@ import (
"github.com/pkg/errors"
"github.com/goreleaser/nfpm/internal/files"
"github.com/goreleaser/nfpm/internal/sign"
"github.com/goreleaser/chglog"
@ -80,27 +81,60 @@ func (*Deb) Package(info *nfpm.Info, deb io.Writer) (err error) {
if ok {
info.Arch = arch
}
dataTarGz, md5sums, instSize, err := createDataTarGz(info)
if err != nil {
return err
}
controlTarGz, err := createControl(instSize, md5sums, info)
if err != nil {
return err
}
debianBinary := []byte("2.0\n")
var w = ar.NewWriter(deb)
if err := w.WriteGlobalHeader(); err != nil {
return errors.Wrap(err, "cannot write ar header to deb file")
}
if err := addArFile(w, "debian-binary", []byte("2.0\n")); err != nil {
if err := addArFile(w, "debian-binary", debianBinary); err != nil {
return errors.Wrap(err, "cannot pack debian-binary")
}
if err := addArFile(w, "control.tar.gz", controlTarGz); err != nil {
return errors.Wrap(err, "cannot add control.tar.gz to deb")
}
if err := addArFile(w, "data.tar.gz", dataTarGz); err != nil {
return errors.Wrap(err, "cannot add data.tar.gz to deb")
}
if info.Deb.Signature.KeyFile != "" {
data := io.MultiReader(bytes.NewReader(debianBinary), bytes.NewReader(controlTarGz),
bytes.NewReader(dataTarGz))
sig, err := sign.PGPArmoredDetachSign(data, info.Deb.Signature.KeyFile,
info.Deb.Signature.KeyPassphrase)
if err != nil {
return &nfpm.ErrSigningFailure{Err: err}
}
sigType := "origin"
if info.Deb.Signature.Type != "" {
sigType = info.Deb.Signature.Type
}
if sigType != "origin" && sigType != "maint" && sigType != "archive" {
return &nfpm.ErrSigningFailure{Err: errors.New("invalid signature type")}
}
if err := addArFile(w, "_gpg"+sigType, sig); err != nil {
return &nfpm.ErrSigningFailure{Err: errors.Wrap(err, "add signature to ar file")}
}
}
return nil
}

View File

@ -6,6 +6,7 @@ import (
"compress/gzip"
"crypto/md5" // nolint: gosec
"encoding/hex"
"errors"
"flag"
"fmt"
"io"
@ -17,11 +18,14 @@ import (
"strings"
"testing"
"github.com/blakesmith/ar"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/goreleaser/chglog"
"github.com/goreleaser/nfpm/internal/sign"
"github.com/goreleaser/nfpm"
)
@ -729,6 +733,49 @@ func testRelativePathPrefixInTarGzFiles(t *testing.T, tarGzFile []byte) {
}
}
func TestDebsigsSignature(t *testing.T) {
info := exampleInfo()
info.Deb.Signature.KeyFile = "../internal/sign/testdata/privkey.asc"
info.Deb.Signature.KeyPassphrase = "hunter2"
var deb bytes.Buffer
err := Default.Package(info, &deb)
require.NoError(t, err)
debBinary, err := extractFileFromAr(deb.Bytes(), "debian-binary")
require.NoError(t, err)
controlTarGz, err := extractFileFromAr(deb.Bytes(), "control.tar.gz")
require.NoError(t, err)
dataTarGz, err := extractFileFromAr(deb.Bytes(), "data.tar.gz")
require.NoError(t, err)
signature, err := extractFileFromAr(deb.Bytes(), "_gpgorigin")
require.NoError(t, err)
message := io.MultiReader(bytes.NewReader(debBinary),
bytes.NewReader(controlTarGz), bytes.NewReader(dataTarGz))
err = sign.PGPVerify(message, signature, "../internal/sign/testdata/pubkey.asc")
require.NoError(t, err)
}
func TestDebsigsSignatureError(t *testing.T) {
info := exampleInfo()
info.Deb.Signature.KeyFile = "/does/not/exist"
var deb bytes.Buffer
err := Default.Package(info, &deb)
require.Error(t, err)
var expectedError *nfpm.ErrSigningFailure
require.True(t, errors.As(err, &expectedError))
_, ok := err.(*nfpm.ErrSigningFailure)
assert.True(t, ok)
}
func extractFileFromTarGz(tarGzFile []byte, filename string) ([]byte, error) {
tarFile, err := gzipInflate(tarGzFile)
if err != nil {
@ -844,3 +891,29 @@ func symlinkTo(tb testing.TB, fileName string) string {
return symlinkName
}
func extractFileFromAr(arFile []byte, filename string) ([]byte, error) {
tr := ar.NewReader(bytes.NewReader(arFile))
for {
hdr, err := tr.Next()
if err == io.EOF {
break // End of archive
}
if err != nil {
return nil, err
}
if path.Join("/", hdr.Name) != path.Join("/", filename) {
continue
}
fileContents, err := ioutil.ReadAll(tr)
if err != nil {
return nil, err
}
return fileContents, nil
}
return nil, os.ErrNotExist
}

3
go.mod
View File

@ -2,6 +2,8 @@ module github.com/goreleaser/nfpm
go 1.14
replace github.com/google/rpmpack => github.com/goreleaser/rpmpack v0.0.0-20200915084912-ac8a7d0c1fdc
require (
github.com/Masterminds/semver/v3 v3.1.0
github.com/alecthomas/kingpin v2.2.6+incompatible
@ -19,6 +21,7 @@ require (
github.com/stretchr/objx v0.2.0 // indirect
github.com/stretchr/testify v1.6.1
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899
gopkg.in/yaml.v2 v2.3.0
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c // indirect
)

6
go.sum
View File

@ -203,6 +203,12 @@ github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGa
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/goreleaser/chglog v0.1.1 h1:UaY3enMEVeavOaZyCraLn+2iM7/2T0yji8Mh7ZFsDp4=
github.com/goreleaser/chglog v0.1.1/go.mod h1:xSDa/73C0TxBcLvoT2JHh47QyXpCx5rrNVzJKyeFGPw=
github.com/goreleaser/rpmpack v0.0.0-20200731134257-3685799e8fdf h1:+QCryOyAhxMBMtB90ALMxTb2C822fon+hsY6av1sdIk=
github.com/goreleaser/rpmpack v0.0.0-20200731134257-3685799e8fdf/go.mod h1:+y9lKiqDhR4zkLl+V9h4q0rdyrYVsWWm6LLCQP33DIk=
github.com/goreleaser/rpmpack v0.0.0-20200912130434-2762bbe52570 h1:xR3+O5ERbBVojh+NPOFNV7gvtrU9rLkblJtzaaFB3yA=
github.com/goreleaser/rpmpack v0.0.0-20200912130434-2762bbe52570/go.mod h1:+y9lKiqDhR4zkLl+V9h4q0rdyrYVsWWm6LLCQP33DIk=
github.com/goreleaser/rpmpack v0.0.0-20200915084912-ac8a7d0c1fdc h1:bYP7CnnBF4FGZUWg6VcEMmGLPGNkwTmEPgoQlgSiaRM=
github.com/goreleaser/rpmpack v0.0.0-20200915084912-ac8a7d0c1fdc/go.mod h1:+y9lKiqDhR4zkLl+V9h4q0rdyrYVsWWm6LLCQP33DIk=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE=

148
internal/sign/pgp.go Normal file
View File

@ -0,0 +1,148 @@
package sign
import (
"bytes"
"io"
"io/ioutil"
"unicode"
"github.com/pkg/errors"
"golang.org/x/crypto/openpgp"
"github.com/goreleaser/nfpm"
)
// PGPSigner returns a PGP signer that creates a detached non-ASCII-armored
// signature and is compatible with rpmpack's signature API.
func PGPSigner(keyFile, passphrase string) func([]byte) ([]byte, error) {
return func(data []byte) ([]byte, error) {
key, err := readSigningKey(keyFile, passphrase)
if err != nil {
return nil, &nfpm.ErrSigningFailure{Err: err}
}
var signature bytes.Buffer
err = openpgp.DetachSign(&signature, key, bytes.NewReader(data), nil)
if err != nil {
return nil, &nfpm.ErrSigningFailure{Err: err}
}
return signature.Bytes(), nil
}
}
// PGPArmoredDetachSign creates an ASCII-armored detached signature.
func PGPArmoredDetachSign(message io.Reader, keyFile, passphrase string) ([]byte, error) {
key, err := readSigningKey(keyFile, passphrase)
if err != nil {
return nil, errors.Wrap(err, "armored detach sign")
}
var signature bytes.Buffer
err = openpgp.ArmoredDetachSign(&signature, key, message, nil)
if err != nil {
return nil, errors.Wrap(err, "armored detach sign")
}
return signature.Bytes(), nil
}
// PGPVerify is exported for use in tests and verifies a ASCII-armored or non-ASCII-armored
// signature using an ASCII-armored or non-ASCII-armored public key file. The signer
// identity is not explicitly checked, other that the obvious fact that the signer's key must
// be in the armoredPubKeyFile.
func PGPVerify(message io.Reader, signature []byte, armoredPubKeyFile string) error {
keyFileContent, err := ioutil.ReadFile(armoredPubKeyFile)
if err != nil {
return errors.Wrap(err, "reading armored public key file")
}
var keyring openpgp.EntityList
if isASCII(keyFileContent) {
keyring, err = openpgp.ReadArmoredKeyRing(bytes.NewReader(keyFileContent))
if err != nil {
return errors.Wrap(err, "decoding armored public key file")
}
} else {
keyring, err = openpgp.ReadKeyRing(bytes.NewReader(keyFileContent))
if err != nil {
return errors.Wrap(err, "decoding public key file")
}
}
if isASCII(signature) {
_, err = openpgp.CheckArmoredDetachedSignature(keyring, message, bytes.NewReader(signature))
return err
}
_, err = openpgp.CheckDetachedSignature(keyring, message, bytes.NewReader(signature))
return err
}
func readSigningKey(keyFile, passphrase string) (*openpgp.Entity, error) {
fileContent, err := ioutil.ReadFile(keyFile)
if err != nil {
return nil, errors.Wrap(err, "reading PGP key file")
}
var entityList openpgp.EntityList
if isASCII(fileContent) {
entityList, err = openpgp.ReadArmoredKeyRing(bytes.NewReader(fileContent))
if err != nil {
return nil, errors.Wrap(err, "decoding armored PGP keyring")
}
} else {
entityList, err = openpgp.ReadKeyRing(bytes.NewReader(fileContent))
if err != nil {
return nil, errors.Wrap(err, "decoding PGP keyring")
}
}
var key *openpgp.Entity
for _, candidate := range entityList {
if candidate.PrivateKey == nil {
continue
}
if !candidate.PrivateKey.CanSign() {
continue
}
if key != nil {
return nil, errors.New("more than one signing key in keyring")
}
key = candidate
}
if key == nil {
return nil, errors.New("no signing key in keyring")
}
if key.PrivateKey.Encrypted {
if passphrase == "" {
return nil, errors.New("key is encrypted but no passphrase was provided")
}
err = key.PrivateKey.Decrypt([]byte(passphrase))
if err != nil {
return nil, errors.Wrap(err, "decrypt secret signing key")
}
}
return key, nil
}
func isASCII(s []byte) bool {
for i := 0; i < len(s); i++ {
if s[i] > unicode.MaxASCII {
return false
}
}
return true
}

145
internal/sign/pgp_test.go Normal file
View File

@ -0,0 +1,145 @@
package sign
import (
"bytes"
"errors"
"io/ioutil"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/openpgp"
"github.com/goreleaser/nfpm"
)
const pass = "hunter2"
func TestPGPSignerAndVerify(t *testing.T) {
data := []byte("testdata")
verifierKeyring := readArmoredKeyring(t, "testdata/pubkey.asc")
testCases := []struct {
name string
keyFile string
pass string
}{
{"protected", "testdata/privkey.gpg", pass},
{"unprotected", "testdata/privkey_unprotected.gpg", ""},
{"armored protected", "testdata/privkey.asc", pass},
{"armored unprotected", "testdata/privkey_unprotected.asc", ""},
}
for _, testCase := range testCases {
testCase := testCase
t.Run(testCase.name, func(t *testing.T) {
sig, err := PGPSigner(testCase.keyFile, testCase.pass)(data)
require.NoError(t, err)
_, err = openpgp.CheckDetachedSignature(verifierKeyring,
bytes.NewReader(data), bytes.NewReader(sig))
assert.NoError(t, err)
err = PGPVerify(bytes.NewReader(data), sig, "testdata/pubkey.asc")
assert.NoError(t, err)
err = PGPVerify(bytes.NewReader(data), sig, "testdata/pubkey.gpg")
assert.NoError(t, err)
})
}
}
func TestArmoredDetachSignAndVerify(t *testing.T) {
data := []byte("testdata")
verifierKeyring := readArmoredKeyring(t, "testdata/pubkey.asc")
testCases := []struct {
name string
keyFile string
pass string
}{
{"protected", "testdata/privkey.gpg", pass},
{"unprotected", "testdata/privkey_unprotected.gpg", ""},
{"armored protected", "testdata/privkey.asc", pass},
{"armored unprotected", "testdata/privkey_unprotected.asc", ""},
}
for _, testCase := range testCases {
testCase := testCase
t.Run(testCase.name, func(t *testing.T) {
sig, err := PGPArmoredDetachSign(bytes.NewReader(data),
testCase.keyFile, testCase.pass)
require.NoError(t, err)
_, err = openpgp.CheckArmoredDetachedSignature(verifierKeyring,
bytes.NewReader(data), bytes.NewReader(sig))
assert.NoError(t, err)
err = PGPVerify(bytes.NewReader(data), sig, "testdata/pubkey.asc")
assert.NoError(t, err)
err = PGPVerify(bytes.NewReader(data), sig, "testdata/pubkey.gpg")
assert.NoError(t, err)
})
}
}
func readArmoredKeyring(t *testing.T, fileName string) openpgp.EntityList {
t.Helper()
content, err := ioutil.ReadFile(fileName)
require.NoError(t, err)
keyring, err := openpgp.ReadArmoredKeyRing(bytes.NewReader(content))
require.NoError(t, err)
return keyring
}
func TestPGPSignerError(t *testing.T) {
_, err := PGPSigner("/does/not/exist", "")([]byte("data"))
require.Error(t, err)
var expectedError *nfpm.ErrSigningFailure
assert.True(t, errors.As(err, &expectedError))
}
func TestNoSigningKey(t *testing.T) {
_, err := readSigningKey("testdata/pubkey.asc", pass)
require.EqualError(t, err, "no signing key in keyring")
}
func TestMultipleKeys(t *testing.T) {
_, err := readSigningKey("testdata/multiple_privkeys.asc", pass)
require.EqualError(t, err, "more than one signing key in keyring")
}
func TestWrongPass(t *testing.T) {
_, err := readSigningKey("testdata/privkey.asc", "password123")
require.EqualError(t, err,
"decrypt secret signing key: openpgp: invalid data: private key checksum failure")
}
func TestEmptyPass(t *testing.T) {
_, err := readSigningKey("testdata/privkey.asc", "")
require.EqualError(t, err, "key is encrypted but no passphrase was provided")
}
func TestReadArmoredKey(t *testing.T) {
_, err := readSigningKey("testdata/privkey.asc", pass)
require.NoError(t, err)
}
func TestReadKey(t *testing.T) {
_, err := readSigningKey("testdata/privkey.gpg", pass)
require.NoError(t, err)
}
func TestIsASCII(t *testing.T) {
data, err := ioutil.ReadFile("testdata/privkey.asc")
require.NoError(t, err)
assert.True(t, isASCII(data))
data, err = ioutil.ReadFile("testdata/privkey.gpg")
require.NoError(t, err)
assert.False(t, isASCII(data))
}

115
internal/sign/rsa.go Normal file
View File

@ -0,0 +1,115 @@
package sign
import (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha1" // nolint:gosec
"crypto/x509"
"encoding/pem"
"io"
"io/ioutil"
"github.com/pkg/errors"
)
// RSASignSHA1Digest signs the provided SHA1 message digest. The key file
// must be in the PEM format and can either be encrypted or not.
func RSASignSHA1Digest(sha1Digest []byte, keyFile, passphrase string) ([]byte, error) {
if len(sha1Digest) != sha1.Size {
return nil, errors.New("digest is not a SHA1 hash")
}
keyFileContent, err := ioutil.ReadFile(keyFile)
if err != nil {
return nil, errors.Wrap(err, "reading key file")
}
block, _ := pem.Decode(keyFileContent)
if block == nil {
return nil, errors.New("parse PEM block with private key")
}
blockData := block.Bytes
if x509.IsEncryptedPEMBlock(block) {
if passphrase == "" {
return nil, errors.New("key is encrypted but no passphrase was provided")
}
var decryptedBlockData []byte
decryptedBlockData, err = x509.DecryptPEMBlock(block, []byte(passphrase))
if err != nil {
return nil, errors.Wrap(err, "decrypt private key PEM block")
}
blockData = decryptedBlockData
}
priv, err := x509.ParsePKCS1PrivateKey(blockData)
if err != nil {
return nil, errors.Wrap(err, "parse PKCS1 private key")
}
signature, err := priv.Sign(rand.Reader, sha1Digest, crypto.SHA1)
if err != nil {
return nil, errors.Wrap(err, "signing")
}
return signature, nil
}
func rsaSign(message io.Reader, keyFile, passphrase string) ([]byte, error) {
sha256Hash := sha1.New() // nolint:gosec
_, err := io.Copy(sha256Hash, message)
if err != nil {
return nil, errors.Wrap(err, "create SHA256 message digest")
}
return RSASignSHA1Digest(sha256Hash.Sum(nil), keyFile, passphrase)
}
// RSAVerifySHA1Digest is exported for use in tests and verifies a signature over the
// provided SHA1 hash of a message. The key file must be in the PEM format.
func RSAVerifySHA1Digest(sha1Digest, signature []byte, publicKeyFile string) error {
if len(sha1Digest) != sha1.Size {
return errors.New("digest is not a SHA1 hash")
}
keyFileContent, err := ioutil.ReadFile(publicKeyFile)
if err != nil {
return errors.Wrap(err, "reading key file")
}
block, _ := pem.Decode(keyFileContent)
if block == nil {
return errors.New("parse PEM block with public key")
}
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return errors.Wrap(err, "parse PKIX public key")
}
rsaPub, ok := pub.(*rsa.PublicKey)
if !ok {
return errors.Wrap(err, "public key is no RSA key")
}
err = rsa.VerifyPKCS1v15(rsaPub, crypto.SHA1, sha1Digest, signature)
if err != nil {
return errors.Wrap(err, "verify PKCS1v15 signature")
}
return nil
}
func rsaVerify(message io.Reader, signature []byte, publicKeyFile string) error {
sha256Hash := sha1.New() // nolint:gosec
_, err := io.Copy(sha256Hash, message)
if err != nil {
return errors.Wrap(err, "create SHA1 message digest")
}
return RSAVerifySHA1Digest(sha256Hash.Sum(nil), signature, publicKeyFile)
}

51
internal/sign/rsa_test.go Normal file
View File

@ -0,0 +1,51 @@
package sign
import (
"bytes"
"testing"
"github.com/stretchr/testify/require"
)
func TestRSASignAndVerify(t *testing.T) {
testData := []byte("test")
testCases := []struct {
name string
privKey string
pubKey string
passphrase string
}{
{"unprotected", "testdata/rsa_unprotected.priv", "testdata/rsa_unprotected.pub", ""},
{"protected", "testdata/rsa.priv", "testdata/rsa.pub", pass},
}
for _, testCase := range testCases {
testCase := testCase
t.Run(testCase.name, func(t *testing.T) {
sig, err := rsaSign(bytes.NewReader(testData), testCase.privKey, testCase.passphrase)
require.NoError(t, err)
err = rsaVerify(bytes.NewReader(testData), sig, testCase.pubKey)
require.NoError(t, err)
})
}
}
func TestWrongPassphrase(t *testing.T) {
testData := []byte("test")
_, err := rsaSign(bytes.NewReader(testData), "testdata/rsa.priv", "password123")
require.EqualError(t, err, "decrypt private key PEM block: x509: decryption password incorrect")
}
func TestNoPassphrase(t *testing.T) {
testData := []byte("test")
_, err := rsaSign(bytes.NewReader(testData), "testdata/rsa.priv", "")
require.EqualError(t, err, "key is encrypted but no passphrase was provided")
}
func TestInvalidHash(t *testing.T) {
invalidDigest := []byte("test")
_, err := RSASignSHA1Digest(invalidDigest, "testdata/rsa.priv", "hunter2")
require.EqualError(t, err, "digest is not a SHA1 hash")
}

View File

@ -0,0 +1,127 @@
-----BEGIN PGP PRIVATE KEY BLOCK-----
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
CeA6I6SIMQKWAsosxcLD
=Uei0
-----END PGP PRIVATE KEY BLOCK-----

66
internal/sign/testdata/privkey.asc vendored Normal file
View File

@ -0,0 +1,66 @@
-----BEGIN PGP PRIVATE KEY BLOCK-----
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
R9YJMg==
=/NbH
-----END PGP PRIVATE KEY BLOCK-----

BIN
internal/sign/testdata/privkey.gpg vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,64 @@
-----BEGIN PGP PRIVATE KEY BLOCK-----
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
WCY7Z0fWCTI=
=dvS6
-----END PGP PRIVATE KEY BLOCK-----

Binary file not shown.

37
internal/sign/testdata/pubkey.asc vendored Normal file
View File

@ -0,0 +1,37 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
CTI=
=arXz
-----END PGP PUBLIC KEY BLOCK-----

BIN
internal/sign/testdata/pubkey.gpg vendored Normal file

Binary file not shown.

30
internal/sign/testdata/rsa.priv vendored Normal file
View File

@ -0,0 +1,30 @@
-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-128-CBC,F681796F2F5F6592720D154E441631AF
U4aCJ0bGTN+/l162THI8CQdVmn62QX7+CxGOrxz8vbPOcMic8ppBD/h4lcUmca1L
B3q9gBV+nM4KWOUjKDsmXJ7PeHrQJxR30RbGKGOP8WZSlf7oqICHX9WfjzsaLmMC
zr7z7osO4DhW0nkr+CJATplKxjF5kry2jsBohmG7AJKVB6JRenpyz/icLrwfeywo
dM1aZEw2Tm6Aso+un+rdmRxvYG5SEVLVh5M334MvOwhKHNGGfNOUJG38uym/sYXM
WbBRyOUhQXabbuHlJdr5V7mnxm+KLi+hZQAIoRsFTS7tkIUpFjtvCS0RI0L6MtkL
3N76xiW82n3SxrPdZdJCGy9AWor66QTmr25yTZpUJMYqpHaXc2pKqZOWZ/U0bAbA
Oyf/CCTwI9lLT1a+CtGTHYTXHHKhQ9n1TI2FI4DRhnDcdDix2S/shF8aeu7mcKF1
SqS8r+DTKzmbohUVa0/Ad9Z6R8Q1Ed+fl2/l0/eb3tI7c7hH+AxV+AqSAgLsZ1/T
SQoIjUX4Kna0U2wLi7wEWlwrQQE2lugoJ2tOH6F33tEd4FFh4CjTw4LKKFEIgjPj
yNKJ5CC+5jQPdyN/utR6Q+PNlxnT5jU02dAupdGfS/On2ecONmBKFCHHT+Sh3F21
Q8zKZPKTUgIqG8JrjIaO1akjJqGgr6c0kefVTYpHEH3HNNkqUvzywIMR7IooPJrD
XQYLMgTuE4ccJk7JN5OCHlSkTHPcrkuD6rRmi7v9zIOMEmwfkN0Ea3HgNJ/7W8gL
mnv47HtwEuuXpyhyQ5xpaJcNWfSaUvSHBr33Ni8rDL2iJu22zEeZXlOU1t4h7wCA
VGd3GBk4n5PILABwILDWKP16bvB3I491QK6m7LehawJNMGDC4Yk379Havcl6BzSX
Rs6NOFcAGyM0VS97HYj3ju7fecdvFwmd86CA92CoK6JbafsQ6o/sOZOhxnrg6nLu
bS26TPwLoqNHrAzg2vTLosbiG3ezge7VuL2GBbQcZm4SsotNLbfpZIhn3JV9/Ivg
rkiUNOEExd52+VImeAJ15Pl6lOb2uAdXwZNsPXfBbanfc5qoggJJlU/xCnQyrPNW
xCi2y3+SQjjlEqdoQbmEEHrU4zzOSj505qDz/Md18W/aih5n1VdzI7Z1YeMAzlpe
X7cGHrQD83AsAtWWv5af5qkHCRYn3fQDGf4svJprsbYxz7e8oAoyjXpKPVbfZcfk
hqlR0T7yN91upFwHhq8q3ZY/CLsZbLecT28M3G7NgE+LtrF0bKcPrDfOCeWyyVBN
wtxPB0P62Nr4jHs5GVO5D1qyCiDMLsS3mwSz3IZn2xf0pit7+O2uu5yIUeGhUN8k
1Y4RwZyGf1e5y7Ulhb5OAbeDYtUTnvgWOqwj5grmACQhhWmKiXX6qUHHn54xyC9P
Yn5f0jdGGVRKINgA+KsipDs3OjMY0D9Dyx3ao646wX+1ypNkUq+hZhykuKlTgqi6
29LdVwXTbcYjc25hbIj9TrXZS7swmwtUhpLwtFyjqrCSMjHW6f6n27Vq8cvL788L
7SbWmpZlxaZA2lj3yjjYnY2bSQBfmA45fF7tBZfHq/ObWFbpvCOU5LMwKjffoPpC
-----END RSA PRIVATE KEY-----

9
internal/sign/testdata/rsa.pub vendored Normal file
View File

@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA53JMl3py0/sBU1y3LBgn
Ffb/GYHATaCiS/Dvoxp8cDTvm2NFGtqCAeyclD3qQAIrZD0igXfi6qeONw3jwea6
4sHE5mHv0ZvkOKqc1G8SE8WPGUiGuk8xsSY/nILFzdqH1yKz81Sv0Y6ti65Gi3Jo
AQPn3DfoDuvyye2hwzXsMTIkfgTj+Mnczsv4Ji1CYKrR8GqlBkePR1hbbeJcWnxb
gqsI2jml9rqx28Fc3fxlkNwG5I619MSN0o1cegKq2gSgodd/EDjqhGF/Fw5nfCGI
Cel3qPPNY/T6mk2P3oqY95YIKXtjeaSloZAkqPw+bl6B0wowigus6wuQ5rx1gN11
fQIDAQAB
-----END PUBLIC KEY-----

View File

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAxWFb75R71bdD12OL9Ba/Z9/61byXEMatIpliLZZKf2Ul937S
IGLECGKvw3DIlQfjdv/u6RIXLQL7ybKKT7R0V8yZK6yhGXnjarPzeoOjaQBqbzR8
tXdJJdpy9vbfTVbFonzb/e8uSgGtoKPwDosL6JjcRz9hLl2B/UaHPhIEoqO4n6tO
BeqcTJwAYV5YrPA+J1Tb8BBpuXPttUFFvXh4bqeB/uLfCorMDP/OnQ51waVb6zxj
HgeY+R35X885rb6XHCTbKPveEeIFkC13TiO9Td9d34lvNX+2z+hlS997Gb8n1uGT
Gek4iEX/k0baI/FUYWzQtIUK7okWJlgSZg7Q4wIDAQABAoIBACPfcP0XCHasKylP
T1VaeQkg/Q6xiucipL7gD6bnZLzInmDf7MK0EJrRjM5BhkMHvwCWvU4g2pN0slxT
Fa4sxuRaLBBy/IiLzZhMAllm6WEhETcK3AuDFYlKrZBTB/yfY6YHeVgqi0gqC+th
QDT0coKgnZBZZYkut3xXwCa40ZzZQHQkGduu7DzNSVTc3P1PoC6FovgJ0G/6d5sa
p+b+c6/2jp15P8OMAUYCibZUDO1EqXAnfTeseW5Lk6cjfwjb+EO9x45AU44t0lk9
GNUPQbsymURAal5tXUDyRMZ0JONtYj70n8rW/Wkxm7VN32KdGJTP3F2qx3VBL8Dd
puFOOvkCgYEA+xV2sY3shIWysZadpsM5I5kFkAsKUV5hl0zF8QJ5VYztVGGMK59N
lXQ09PWEzCzi9H8um9prVPGucR+vdPEbgJ0zufZ6h5StO9+i01A7qpBiHezyXwls
KJ3fdEybT/7cNohreoJp3D92TBKkYuE7B6Mw0aLwdkgUU3Z0FPj9oN0CgYEAyT62
Cl2gK9ts+CxN10j6tZRwZ/4cSXMaQXvBT6prCWHnmfV2OcC+AGnQNTdkzAFvfcdK
xEtrPRICiy37JCKvOoYemhfg1ZCVpO+Md0iXhd31K1bcTKf3XSRwtt2Av8INGM2H
d7Vrie5ZFbnmeb5I9kegwCBoQowXlty8ummgPL8CgYEAuH6BBN6gWJBVSaC+nvb8
WWfranhyUG/ljeNf30ROG8q96S70bGlV/OeysfxmZBDng3igquzHyVb9MypxJqCz
8MGSbKbsSgu5bzFY16Uw8pe9QPgG8EZgL0gyFabkyNcgwQFk2FRTrP8E0ckw+Wc+
lNIY4TG1N3SnWtzDV1XOKBECgYEAs2AZRI4FSXIw49ghneXnydfAhYDjV7iny3xy
Zv8AnwFMPVekBIA/GwE122k/h/MznEn545NRx8J4z/OxKZlc1O5c8n6cId/Y8oWu
j1f97w1TDUlD3XPNsZDcgrJWoxVlNRllSRdE/lGbyBjGTPeF8zcPGpEIPWC/WfHw
qruhXuMCgYBR5LpIe0jXMo2RgSkLT2YVomy/3ZjaU736DLUwjv4quXjrkW/JTrv6
APCQXVwcHrn0sJmjyXTlBAXuzpBx/IUiQ7qGxSNg6ZpDmZq0imqnsXXRZBUShnnT
JLH0Qy1f8hcTFvLUfVyLiQaj+2XpKaCnb2Bc3YRZ8TRrwx6HeRT0Ug==
-----END RSA PRIVATE KEY-----

View File

@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxWFb75R71bdD12OL9Ba/
Z9/61byXEMatIpliLZZKf2Ul937SIGLECGKvw3DIlQfjdv/u6RIXLQL7ybKKT7R0
V8yZK6yhGXnjarPzeoOjaQBqbzR8tXdJJdpy9vbfTVbFonzb/e8uSgGtoKPwDosL
6JjcRz9hLl2B/UaHPhIEoqO4n6tOBeqcTJwAYV5YrPA+J1Tb8BBpuXPttUFFvXh4
bqeB/uLfCorMDP/OnQ51waVb6zxjHgeY+R35X885rb6XHCTbKPveEeIFkC13TiO9
Td9d34lvNX+2z+hlS997Gb8n1uGTGek4iEX/k0baI/FUYWzQtIUK7okWJlgSZg7Q
4wIDAQAB
-----END PUBLIC KEY-----

73
nfpm.go
View File

@ -59,6 +59,26 @@ func Parse(in io.Reader) (config Config, err error) {
config.Info.Release = os.ExpandEnv(config.Info.Release)
config.Info.Version = os.ExpandEnv(config.Info.Version)
generalPassphrase := os.ExpandEnv("$NFPM_PASSPHRASE")
config.Deb.Signature.KeyPassphrase = generalPassphrase
config.RPM.Signature.KeyPassphrase = generalPassphrase
config.APK.Signature.KeyPassphrase = generalPassphrase
debPassphrase := os.ExpandEnv("$NFPM_DEB_PASSPHRASE")
if debPassphrase != "" {
config.Deb.Signature.KeyPassphrase = debPassphrase
}
rpmPassphrase := os.ExpandEnv("$NFPM_RPM_PASSPHRASE")
if rpmPassphrase != "" {
config.RPM.Signature.KeyPassphrase = rpmPassphrase
}
apkPassphrase := os.ExpandEnv("$NFPM_APK_PASSPHRASE")
if apkPassphrase != "" {
config.APK.Signature.KeyPassphrase = apkPassphrase
}
return config, config.Validate()
}
@ -152,6 +172,7 @@ type Overridables struct {
Scripts Scripts `yaml:"scripts,omitempty"`
RPM RPM `yaml:"rpm,omitempty"`
Deb Deb `yaml:"deb,omitempty"`
APK APK `yaml:"apk,omitempty"`
}
// RPM is custom configs that are only available on RPM packages.
@ -160,14 +181,42 @@ type RPM struct {
Compression string `yaml:"compression,omitempty"`
// https://www.cl.cam.ac.uk/~jw35/docs/rpm_config.html
ConfigNoReplaceFiles map[string]string `yaml:"config_noreplace_files,omitempty"`
Signature RPMSignature `yaml:"signature,omitempty"`
}
type RPMSignature struct {
// PGP secret key, can be ASCII-armored
KeyFile string `yaml:"key_file,omitempty"`
KeyPassphrase string `yaml:"-"` // populated from environment variable
}
type APK struct {
Signature APKSignature `yaml:"signature,omitempty"`
}
type APKSignature struct {
// RSA private key in PEM format
KeyFile string `yaml:"key_file,omitempty"`
KeyPassphrase string `yaml:"-"` // populated from environment variable
// defaults to <maintainer email>.rsa.pub
KeyName string `yaml:"key_name,omitempty"`
}
// Deb is custom configs that are only available on deb packages.
type Deb struct {
Scripts DebScripts `yaml:"scripts,omitempty"`
Triggers DebTriggers `yaml:"triggers,omitempty"`
Breaks []string `yaml:"breaks,omitempty"`
VersionMetadata string `yaml:"metadata,omitempty"` // Deprecated: Moved to Info
Scripts DebScripts `yaml:"scripts,omitempty"`
Triggers DebTriggers `yaml:"triggers,omitempty"`
Breaks []string `yaml:"breaks,omitempty"`
VersionMetadata string `yaml:"metadata,omitempty"` // Deprecated: Moved to Info
Signature DebSignature `yaml:"signature,omitempty"`
}
type DebSignature struct {
// PGP secret key, can be ASCII-armored
KeyFile string `yaml:"key_file,omitempty"`
KeyPassphrase string `yaml:"-"` // populated from environment variable
// origin, maint or archive (defaults to origin)
Type string `yaml:"type,omitempty"`
}
// DebTriggers contains triggers only available for deb packages.
@ -273,3 +322,19 @@ func (info *Info) GetChangeLog() (log *chglog.PackageChangeLog, err error) {
Entries: entries,
}, nil
}
// ErrSigningFailure is returned whenever something went wrong during
// the package signing process. The underlying error can be unwrapped
// and could be crypto-related or something that occurred while adding
// the signature to the package.
type ErrSigningFailure struct {
Err error
}
func (s *ErrSigningFailure) Error() string {
return fmt.Sprintf("signing error: %v", s.Err)
}
func (s *ErrSigningFailure) Unwarp() error {
return s.Err
}

View File

@ -5,6 +5,7 @@ import (
"io"
"os"
"reflect"
"strings"
"testing"
"github.com/stretchr/testify/assert"
@ -149,6 +150,56 @@ func TestParseFile(t *testing.T) {
assert.Equal(t, fmt.Sprintf("v%s", os.Getenv("GOROOT")), config.Version)
}
func TestOptionsFromEnvironment(t *testing.T) {
const (
globalPass = "hunter2"
debPass = "password123"
rpmPass = "secret"
apkPass = "foobar"
release = "3"
version = "1.0.0"
)
t.Run("version", func(t *testing.T) {
os.Clearenv()
os.Setenv("VERSION", version)
info, err := Parse(strings.NewReader("name: foo\nversion: $VERSION"))
require.NoError(t, err)
assert.Equal(t, version, info.Version)
})
t.Run("release", func(t *testing.T) {
os.Clearenv()
os.Setenv("RELEASE", release)
info, err := Parse(strings.NewReader("name: foo\nrelease: $RELEASE"))
require.NoError(t, err)
assert.Equal(t, release, info.Release)
})
t.Run("global passphrase", func(t *testing.T) {
os.Clearenv()
os.Setenv("NFPM_PASSPHRASE", globalPass)
info, err := Parse(strings.NewReader("name: foo"))
require.NoError(t, err)
assert.Equal(t, globalPass, info.Deb.Signature.KeyPassphrase)
assert.Equal(t, globalPass, info.RPM.Signature.KeyPassphrase)
assert.Equal(t, globalPass, info.APK.Signature.KeyPassphrase)
})
t.Run("specific passphrases", func(t *testing.T) {
os.Clearenv()
os.Setenv("NFPM_PASSPHRASE", globalPass)
os.Setenv("NFPM_DEB_PASSPHRASE", debPass)
os.Setenv("NFPM_RPM_PASSPHRASE", rpmPass)
os.Setenv("NFPM_APK_PASSPHRASE", apkPass)
info, err := Parse(strings.NewReader("name: foo"))
require.NoError(t, err)
assert.Equal(t, debPass, info.Deb.Signature.KeyPassphrase)
assert.Equal(t, rpmPass, info.RPM.Signature.KeyPassphrase)
assert.Equal(t, apkPass, info.APK.Signature.KeyPassphrase)
})
}
func TestOverrides(t *testing.T) {
file := "./testdata/overrides.yaml"
config, err := ParseFile(file)

View File

@ -17,6 +17,7 @@ import (
"github.com/sassoftware/go-rpmutils/cpio"
"github.com/goreleaser/nfpm/internal/files"
"github.com/goreleaser/nfpm/internal/sign"
"github.com/goreleaser/chglog"
@ -98,6 +99,10 @@ func (*RPM) Package(info *nfpm.Info, w io.Writer) error {
return err
}
if info.RPM.Signature.KeyFile != "" {
rpm.SetPGPSigner(sign.PGPSigner(info.RPM.Signature.KeyFile, info.RPM.Signature.KeyPassphrase))
}
addEmptyDirsRPM(info, rpm)
if err = createFilesInsideRPM(info, rpm); err != nil {
return err

View File

@ -2,6 +2,7 @@ package rpm
import (
"bytes"
"errors"
"fmt"
"io"
"io/ioutil"
@ -16,6 +17,7 @@ import (
"github.com/sassoftware/go-rpmutils/cpio"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/openpgp"
"github.com/goreleaser/chglog"
@ -555,6 +557,39 @@ func TestSymlink(t *testing.T) {
assert.Equal(t, symlinkTarget, string(packagedSymlink))
}
func TestRPMSignature(t *testing.T) {
info := exampleInfo()
info.RPM.Signature.KeyFile = "../internal/sign/testdata/privkey.asc"
info.RPM.Signature.KeyPassphrase = "hunter2"
pubkeyFileContent, err := ioutil.ReadFile("../internal/sign/testdata/pubkey.gpg")
require.NoError(t, err)
keyring, err := openpgp.ReadKeyRing(bytes.NewReader(pubkeyFileContent))
require.NoError(t, err)
var rpmBuffer bytes.Buffer
err = Default.Package(info, &rpmBuffer)
require.NoError(t, err)
_, sigs, err := rpmutils.Verify(bytes.NewReader(rpmBuffer.Bytes()), keyring)
require.NoError(t, err)
require.Len(t, sigs, 1)
}
func TestRPMSignatureError(t *testing.T) {
info := exampleInfo()
info.RPM.Signature.KeyFile = "../internal/sign/testdata/privkey.asc"
info.RPM.Signature.KeyPassphrase = "wrongpass"
var rpmBuffer bytes.Buffer
err := Default.Package(info, &rpmBuffer)
require.Error(t, err)
var expectedError *nfpm.ErrSigningFailure
require.True(t, errors.As(err, &expectedError))
}
func extractFileFromRpm(rpm []byte, filename string) ([]byte, error) {
rpmFile, err := rpmutils.ReadRpm(bytes.NewReader(rpm))
if err != nil {

View File

@ -143,6 +143,13 @@ rpm:
config_noreplace_files:
path/to/local/bar.con: /etc/bar.conf
# The package is signed if a key_file is set
signature:
# PGP secret key (can also be ASCII-armored), the passphrase is taken
# from the environment variable $NFPM_RPM_PASSPHRASE with a fallback
# to #NFPM_PASSPHRASE.
key_file: key.gpg
# Custon configuration applied only to the Deb packager.
deb:
# Custom deb rules script.
@ -165,6 +172,28 @@ deb:
# is already installed.
breaks:
- some-package
# The package is signed if a key_file is set
signature:
# PGP secret key (can also be ASCII-armored). The passphrase is taken
# from the environment variable $NFPM_DEB_PASSPHRASE with a fallback
# to #NFPM_PASSPHRASE.
key_file: key.gpg
# The type describes the signers role, possible values are "origin",
# "maint" and "archive". If unset, the type defaults to "origin".
type: origin
apk:
# The package is signed if a key_file is set
signature:
# RSA private key in the PEM format. The passphrase is taken from
# the environment variable $NFPM_APK_PASSPHRASE with a fallback
# to #NFPM_PASSPHRASE.
key_file: key.gpg
# The name of the signing key. When verifying a package, the signature
# is matched to the public key store in /etc/apk/keys/<key_name>.rsa.pub.
# If unset, it defaults to the maintainer email address.
key_name: origin
```
## Templating