Skip to content

Commit

Permalink
Merge pull request #1198 from tcnghia/sign-as-a-pkg
Browse files Browse the repository at this point in the history
Extract `melange sign` to a library
  • Loading branch information
tcnghia authored May 9, 2024
2 parents cde1a35 + bb0b8ae commit 2e6bd0f
Show file tree
Hide file tree
Showing 7 changed files with 253 additions and 69 deletions.
71 changes: 2 additions & 69 deletions pkg/cli/sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,12 @@ import (
"io"
"os"

pkgsign "chainguard.dev/melange/pkg/sign"
"github.com/chainguard-dev/clog"
"github.com/chainguard-dev/go-apk/pkg/expandapk"
sign "github.com/chainguard-dev/go-apk/pkg/signature"
"github.com/klauspost/compress/gzip"
"github.com/spf13/cobra"
"golang.org/x/sync/errgroup"

"chainguard.dev/melange/pkg/build"
)

type signIndexOpts struct {
Expand Down Expand Up @@ -193,70 +191,5 @@ func (o signOpts) RunAllE(ctx context.Context, pkgs ...string) error {

func (o signOpts) run(ctx context.Context, pkg string) error {
clog.FromContext(ctx).Infof("Processing apk %s", pkg)

apkr, err := os.Open(pkg)
if err != nil {
return err
}

eapk, err := expandapk.ExpandApk(ctx, apkr, "")
if err != nil {
return fmt.Errorf("expanding apk: %w", err)
}
defer eapk.Close()

if err := apkr.Close(); err != nil {
return err
}

// Split the streams and then rebuild
cf, err := os.Open(eapk.ControlFile)
if err != nil {
return err
}
defer cf.Close()

// Use the control sections ModTime (set to SDE) for the signature
cfinfo, err := os.Stat(eapk.ControlFile)
if err != nil {
return err
}

pc := &build.PackageBuild{
Build: &build.Build{
SigningKey: o.Key,
SigningPassphrase: "",
},
}

cdata, err := os.ReadFile(eapk.ControlFile)
if err != nil {
return err
}

sigData, err := build.EmitSignature(ctx, pc.Signer(), cdata, cfinfo.ModTime())
if err != nil {
return err
}

df, err := os.Open(eapk.PackageFile)
if err != nil {
return err
}
defer df.Close()

// Replace the package file with the new one
f, err := os.Create(pkg)
if err != nil {
return err
}
defer f.Close()

for _, fp := range []io.Reader{bytes.NewBuffer(sigData), cf, df} {
if _, err := io.Copy(f, fp); err != nil {
return err
}
}

return nil
return pkgsign.APK(ctx, pkg, o.Key)
}
95 changes: 95 additions & 0 deletions pkg/sign/apk.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Copyright 2024 Chainguard, Inc.
//
// 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 sign

import (
"bytes"
"context"
"fmt"
"io"
"os"

"chainguard.dev/melange/pkg/build"
"github.com/chainguard-dev/go-apk/pkg/expandapk"
)

// APK() signs an APK file with the provided key. The existing APK file is
// replaced with the signed APK file.
func APK(ctx context.Context, apkPath string, keyPath string) error {
apkr, err := os.Open(apkPath)
if err != nil {
return err
}

eapk, err := expandapk.ExpandApk(ctx, apkr, "")
if err != nil {
return fmt.Errorf("expanding apk: %w", err)
}
defer eapk.Close()

if err := apkr.Close(); err != nil {
return err
}

// Split the streams and then rebuild
cf, err := os.Open(eapk.ControlFile)
if err != nil {
return err
}
defer cf.Close()

// Use the control sections ModTime (set to SDE) for the signature
cfinfo, err := os.Stat(eapk.ControlFile)
if err != nil {
return err
}

pc := &build.PackageBuild{
Build: &build.Build{
SigningKey: keyPath,
SigningPassphrase: "",
},
}

cdata, err := os.ReadFile(eapk.ControlFile)
if err != nil {
return err
}

sigData, err := build.EmitSignature(ctx, pc.Signer(), cdata, cfinfo.ModTime())
if err != nil {
return err
}

df, err := os.Open(eapk.PackageFile)
if err != nil {
return err
}
defer df.Close()

// Replace the package file with the new one
f, err := os.Create(apkPath)
if err != nil {
return err
}
defer f.Close()

for _, fp := range []io.Reader{bytes.NewBuffer(sigData), cf, df} {
if _, err := io.Copy(f, fp); err != nil {
return err
}
}

return nil
}
120 changes: 120 additions & 0 deletions pkg/sign/apk_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// Copyright 2024 Chainguard, Inc.
//
// 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 sign

import (
"archive/tar"
"bytes"
"compress/gzip"
"context"
"crypto/sha1"
"fmt"
"io"
"os"
"strings"
"testing"

"github.com/chainguard-dev/go-apk/pkg/expandapk"
"github.com/chainguard-dev/go-apk/pkg/signature"
)

const (
testAPK = "testdata/test.apk"
testPubkey = "test.pem.pub"
testPrivKey = "test.pem"
)

func TestAPK(t *testing.T) {
tmpDir := t.TempDir()
ctx := context.Background()
apkPath := tmpDir + "/out.apk"

// copy testdata/test.apk to tmpDir
if err := CopyFile(testAPK, apkPath); err != nil {
t.Fatal(err)
}
// sign the apk
if err := APK(ctx, apkPath, "testdata/"+testPrivKey); err != nil {
t.Fatal(err)
}
// verify the signature
controlData, sigName, sig, err := parseAPK(ctx, apkPath)
if err != nil {
t.Fatal(err)
}
if sigName != ".SIGN.RSA."+testPubkey {
t.Fatalf("unexpected signature name %s", sigName)
}
//nolint:gosec we do have to use SHA1 here
digest := computeSHA1Digest(controlData)
pubKey, err := os.ReadFile("testdata/" + testPubkey)
if err != nil {
t.Fatal(err)
}
if err := signature.RSAVerifySHA1Digest(digest, sig, pubKey); err != nil {
t.Fatal(err)
}
}

func computeSHA1Digest(data []byte) []byte {
digest := sha1.New()
_, _ = digest.Write(data)
return digest.Sum(nil)
}

func parseAPK(ctx context.Context, apkPath string) (control []byte, sigName string, sig []byte, err error) {
apkr, err := os.Open(apkPath)
if err != nil {
return nil, "", nil, err
}
eapk, err := expandapk.ExpandApk(context.TODO(), apkr, "")
if err != nil {
return nil, "", nil, err
}
defer eapk.Close()
gzSig, err := os.ReadFile(eapk.SignatureFile)
if err != nil {
return nil, "", nil, err
}
zr, err := gzip.NewReader(bytes.NewReader(gzSig))
if err != nil {
return nil, "", nil, err
}
tr := tar.NewReader(zr)
hdr, err := tr.Next()
if err != nil {
return nil, "", nil, err
}
if !strings.HasPrefix(hdr.Name, ".SIGN.") {
return nil, "", nil, fmt.Errorf("unexpected header name %s", hdr.Name)
}
sig, err = io.ReadAll(tr)
control, err = os.ReadFile(eapk.ControlFile)
if err != nil {
return nil, "", nil, err
}
return control, hdr.Name, sig, err
}

func CopyFile(src, dest string) error {
b, err := os.ReadFile(src)
if err != nil {
return err
}
if err := os.WriteFile(dest, b, 0644); err != nil {
return err
}
return nil
}
Binary file added pkg/sign/testdata/.SIGN.RSA.wolfi-signing.rsa.pub
Binary file not shown.
Binary file added pkg/sign/testdata/test.apk
Binary file not shown.
27 changes: 27 additions & 0 deletions pkg/sign/testdata/test.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEoAIBAAKCAQEAm+LrkhrpjcH7xBTm2HvRkEQBOn/VLbCqNmG7+7/WKWaim1bm
Acj/o/grb7NUXUH529aIa+Ue4q8b4g8J8I6rBWZ32f8mQ/iMereQYe0uoyhjb3px
HZZDdj7q0nq5WyYTGl0nCs/0YHed0F442vCqQEBEvOeI4fXx1VWlmc9zX5px6/sF
PQbrS3jAD5AjGgbB5JlW3c3cqWqvQu1/P1t+euOnhLCaCKQXehezwOajlj36qfbR
EyH2SmEk5x2OIo4r5MD9kozBeLJnqaU1svUe3EF7wK/qiLtUYi7V2NkAL2sR0goO
xavovHQZpJFfKw72gXE+I3omHSb/VSPEQuCjwQIDAQABAoH/SHYzCrd32W1SmtMc
e4US5Py3lXnWnmaAuMa1m1CRPK146Lx8LlhAfPffOQ0jKjaA7q/KulzG/phnXa2f
8TJgTSJUlAEGyJHAu0qY0uxtwWoEzs0bx6URtuWIQk7J2pTYTGkAvQXtkuoHcdRa
mWtFuJgnW6hE+MRapdAqKlVETCvE+4bVrkyyF1sEd7XT9W3bKSvKJdRJu+jZ7FWI
oCmyCA062yzgES7u8A6zHpX/4Ti5rY7vkOkJF3KxKTV6nb6USPk1F71+4vyduRk5
/6ZzUatJ4V+JWTVn34pSrZGJln/RAcEWSz/lXZIimKoOl/I/G38+GqAdcX6I6eyn
YKtRAoGBAM3EIfQJuH4CMOYfauLkFvRSzCBKtkeUyDFGSrmeYppFCIqIfcPbP8Ep
tccjGE3VVQ7lEpUoKtPuyQqDISnJT+FSrZoqlR2fKuANKudYdGfKDtouGCiraBWU
dOPN73QfvWd8512XTYMONVBmhHLHtp/apyvoIf3oao3OKUmgyCvtAoGBAMHxbeW6
jADh6PZI7tVcGzoDwpA93I+Q+5YCsNkTpE00P6kD7pL3fAAAzkRwA9xH9q2ZQZGz
XRm/qcdVI+OmNUQNqOrLCbxQ9lBNfreOZe8R8MlDuUfXa3AbSahuGpAG/osgRw+7
oK2AEIhYTA5A6vfhbNBmp0hTsbvxjbHqiySlAoGANhjQjGZZ2NceoAG2ijxJRKbX
/81kquEU2M+QKcjYR5LKshE8b1efJVuf7ODvLNdfa3ESN6C90cY/mMHs4B2LIMQp
3BRB6+3CyfDsTLJWuErJKNdhhp+516KWMKYdxDvsAd82vMZgnIoJvj+Vps+W1eQY
e1SmSyjliq6e8DqTJekCgYBwE8ZPmRNpOyQ2l4U11YmCaEPauXUpnj5VvW5XtEsh
to0HbldDpTOKiOyqjhUdCpVaUxOaUI3/4EwL/n7EAvwLPN2d+gHBSwRc+bu99dOg
aby9gp6jDrFu0sYpSJ+fFfupiNioFeTP/w8Oy///yLJA14vbj0civAMdOoWJCKKq
ZQKBgDExnxyd6YY8is/bSZ25EkHgYe6x1U+BTcC98rJtEqqNG4KDWv8bYG5MJ4+x
yECWBKdgzXLu7jCZ/FMb3/Woq6APWXWKw+oNC92gv0eT/g4VGOor2PTv4MYxFxH9
7+R2On3InvZwQCk/GvubYgeQhBqewWaHrVNf3sLDVgmf++LR
-----END RSA PRIVATE KEY-----
9 changes: 9 additions & 0 deletions pkg/sign/testdata/test.pem.pub
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAm+LrkhrpjcH7xBTm2HvR
kEQBOn/VLbCqNmG7+7/WKWaim1bmAcj/o/grb7NUXUH529aIa+Ue4q8b4g8J8I6r
BWZ32f8mQ/iMereQYe0uoyhjb3pxHZZDdj7q0nq5WyYTGl0nCs/0YHed0F442vCq
QEBEvOeI4fXx1VWlmc9zX5px6/sFPQbrS3jAD5AjGgbB5JlW3c3cqWqvQu1/P1t+
euOnhLCaCKQXehezwOajlj36qfbREyH2SmEk5x2OIo4r5MD9kozBeLJnqaU1svUe
3EF7wK/qiLtUYi7V2NkAL2sR0goOxavovHQZpJFfKw72gXE+I3omHSb/VSPEQuCj
wQIDAQAB
-----END PUBLIC KEY-----

0 comments on commit 2e6bd0f

Please sign in to comment.