Skip to content

Commit

Permalink
Merge pull request #84 from tri-adam/integrity
Browse files Browse the repository at this point in the history
Integrity Package
  • Loading branch information
tri-adam authored Jul 14, 2020
2 parents 75eed70 + e272029 commit f7016b5
Show file tree
Hide file tree
Showing 90 changed files with 5,271 additions and 0 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ go 1.13
require (
github.com/satori/go.uuid v1.2.0
github.com/spf13/cobra v1.0.0
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9
)
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -96,12 +96,15 @@ go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 h1:vEg9joUBmeBcK9iSJftGNf3coIG4HqZElCPehJsfAYM=
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
Expand All @@ -112,6 +115,7 @@ golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
Expand Down
78 changes: 78 additions & 0 deletions pkg/integrity/clearsign.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright (c) 2020, Sylabs Inc. All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the LICENSE.md file
// distributed with the sources of this project regarding your rights to use or distribute this
// software.

package integrity

import (
"bytes"
"encoding/json"
"errors"
"io"

"golang.org/x/crypto/openpgp"
"golang.org/x/crypto/openpgp/clearsign"
"golang.org/x/crypto/openpgp/packet"
)

var errClearsignedMsgNotFound = errors.New("clearsigned message not found")

// signAndEncodeJSON encodes v, clear-signs it with privateKey, and writes it to w. If config is
// nil, sensible defaults are used.
func signAndEncodeJSON(w io.Writer, v interface{}, privateKey *packet.PrivateKey, config *packet.Config) error {
// Get clearsign encoder.
plaintext, err := clearsign.Encode(w, privateKey, config)
if err != nil {
return err
}
defer plaintext.Close()

// Wrap clearsign encoder with JSON encoder.
return json.NewEncoder(plaintext).Encode(v)
}

// verifyAndDecodeJSON reads the first clearsigned message in data, verifies its signature, and
// returns the signing entity any suffix of data which follows the message. The plaintext is
// unmarshalled to v (if not nil).
func verifyAndDecodeJSON(data []byte, v interface{}, kr openpgp.KeyRing) (*openpgp.Entity, []byte, error) {
// Decode clearsign block and check signature.
e, plaintext, rest, err := verifyAndDecode(data, kr)
if err != nil {
return e, rest, err
}

// Unmarshal plaintext, if requested.
if v != nil {
err = json.Unmarshal(plaintext, v)
}
return e, rest, err
}

// verifyAndDecode reads the first clearsigned message in data, verifies its signature, and returns
// the signing entity, plaintext and suffix of data which follows the message.
func verifyAndDecode(data []byte, kr openpgp.KeyRing) (*openpgp.Entity, []byte, []byte, error) {
// Decode clearsign block.
b, rest := clearsign.Decode(data)
if b == nil {
return nil, nil, rest, errClearsignedMsgNotFound
}

// Check signature.
e, err := openpgp.CheckDetachedSignature(kr, bytes.NewReader(b.Bytes), b.ArmoredSignature.Body)
return e, b.Plaintext, rest, err
}

// isLegacySignature reads the first clearsigned message in data, and returns true if the plaintext
// contains a legacy signature.
func isLegacySignature(data []byte) (bool, error) {
// Decode clearsign block.
b, _ := clearsign.Decode(data)
if b == nil {
return false, errClearsignedMsgNotFound
}

// The plaintext of legacy signatures always begins with "SIFHASH", and non-legacy signatures
// never do, as they are JSON.
return bytes.HasPrefix(b.Plaintext, []byte("SIFHASH:\n")), nil
}
183 changes: 183 additions & 0 deletions pkg/integrity/clearsign_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
// Copyright (c) 2020, Sylabs Inc. All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the LICENSE.md file
// distributed with the sources of this project regarding your rights to use or distribute this
// software.

package integrity

import (
"bufio"
"bytes"
"crypto"
"errors"
"io"
"reflect"
"strings"
"testing"

"golang.org/x/crypto/openpgp"
pgperrors "golang.org/x/crypto/openpgp/errors"
"golang.org/x/crypto/openpgp/packet"
)

type testType struct {
One int
Two int
}

func TestSignAndEncodeJSON(t *testing.T) {
e := getTestEntity(t)

// Fake an encrypted key.
encryptedKey := *e.PrivateKey
encryptedKey.Encrypted = true

tests := []struct {
name string
key *packet.PrivateKey
hash crypto.Hash
wantErr bool
}{
{name: "EncryptedKey", key: &encryptedKey, wantErr: true},
{name: "DefaultHash", key: e.PrivateKey},
{name: "SHA1", key: e.PrivateKey, hash: crypto.SHA1},
{name: "SHA224", key: e.PrivateKey, hash: crypto.SHA224},
{name: "SHA256", key: e.PrivateKey, hash: crypto.SHA256},
{name: "SHA384", key: e.PrivateKey, hash: crypto.SHA384},
{name: "SHA512", key: e.PrivateKey, hash: crypto.SHA512},
}

for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
b := bytes.Buffer{}

config := packet.Config{
DefaultHash: tt.hash,
Time: fixedTime,
}

err := signAndEncodeJSON(&b, testType{1, 2}, tt.key, &config)
if got, want := err, tt.wantErr; (got != nil) != want {
t.Fatalf("got error %v, wantErr %v", got, want)
}

if err == nil {
if err := verifyGolden(t.Name(), &b); err != nil {
t.Fatalf("failed to verify golden: %v", err)
}
}
})
}
}

func TestVerifyAndDecodeJSON(t *testing.T) {
e := getTestEntity(t)

testValue := testType{1, 2}

// This is used to corrupt the plaintext.
corruptClearsign := func(w io.Writer, s string) error {
_, err := strings.NewReplacer(`{"One":1,"Two":2}`, `{"One":2,"Two":4}`).WriteString(w, s)
return err
}

// This is used to corrupt the signature.
corruptSignature := func(w io.Writer, s string) error {
sc := bufio.NewScanner(strings.NewReader(s))

for sigFound, n := false, 0; sc.Scan(); {
line := sc.Text()

if sigFound {
if n == 1 {
// Introduce some corruption
line = line[:len(line)-1]
}
n++
} else if line == "-----BEGIN PGP SIGNATURE-----" {
sigFound = true
}

if _, err := io.WriteString(w, line+"\n"); err != nil {
return err
}
}

return nil
}

tests := []struct {
name string
hash crypto.Hash
el openpgp.EntityList
corrupter func(w io.Writer, s string) error
output interface{}
wantErr error
wantEntity *openpgp.Entity
}{
{name: "ErrUnknownIssuer", el: openpgp.EntityList{}, wantErr: pgperrors.ErrUnknownIssuer},
{name: "CorruptedClearsign", el: openpgp.EntityList{e}, corrupter: corruptClearsign},
{name: "CorruptedSignature", el: openpgp.EntityList{e}, corrupter: corruptSignature},
{name: "VerifyOnly", el: openpgp.EntityList{e}, wantEntity: e},
{name: "DefaultHash", el: openpgp.EntityList{e}, output: &testType{}, wantEntity: e},
{name: "SHA1", hash: crypto.SHA1, el: openpgp.EntityList{e}, output: &testType{}, wantEntity: e},
{name: "SHA224", hash: crypto.SHA224, el: openpgp.EntityList{e}, output: &testType{}, wantEntity: e},
{name: "SHA256", hash: crypto.SHA256, el: openpgp.EntityList{e}, output: &testType{}, wantEntity: e},
{name: "SHA384", hash: crypto.SHA384, el: openpgp.EntityList{e}, output: &testType{}, wantEntity: e},
{name: "SHA512", hash: crypto.SHA512, el: openpgp.EntityList{e}, output: &testType{}, wantEntity: e},
}

for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
b := bytes.Buffer{}

config := packet.Config{
DefaultHash: tt.hash,
}
err := signAndEncodeJSON(&b, testValue, e.PrivateKey, &config)
if err != nil {
t.Fatal(err)
}

// Introduce corruption, if applicable.
if tt.corrupter != nil {
s := b.String()
b.Reset()
if err := tt.corrupter(&b, s); err != nil {
t.Fatal(err)
}
}

// Verify and decode.
e, rest, err := verifyAndDecodeJSON(b.Bytes(), tt.output, tt.el)

// Shouldn't be any trailing bytes.
if n := len(rest); n != 0 {
t.Errorf("%v trailing bytes", n)
}

// Verify the error (if any) is appropriate.
if tt.corrupter == nil {
if got, want := err, tt.wantErr; !errors.Is(got, want) {
t.Fatalf("got error %v, want %v", got, want)
}
} else if err == nil {
t.Errorf("got nil error despite corruption")
}

if err == nil {
if tt.output != nil {
if got, want := tt.output, &testValue; !reflect.DeepEqual(got, want) {
t.Errorf("got value %v, want %v", got, want)
}
}

if got, want := e, tt.wantEntity; !reflect.DeepEqual(got, want) {
t.Errorf("got entity %+v, want %+v", got, want)
}
}
})
}
}
Loading

0 comments on commit f7016b5

Please sign in to comment.