-
Notifications
You must be signed in to change notification settings - Fork 24
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #84 from tri-adam/integrity
Integrity Package
- Loading branch information
Showing
90 changed files
with
5,271 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} | ||
}) | ||
} | ||
} |
Oops, something went wrong.