summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-16 17:40:08 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-16 17:40:08 +0000
commit3d4ba0d0807b618fb0d5f1bc6c17a6774f6c9145 (patch)
tree675711124073e227e5c74c9c2543e08cbd9f47db
parentInitial commit. (diff)
downloadgolang-github-secure-systems-lab-go-securesystemslib-3d4ba0d0807b618fb0d5f1bc6c17a6774f6c9145.tar.xz
golang-github-secure-systems-lab-go-securesystemslib-3d4ba0d0807b618fb0d5f1bc6c17a6774f6c9145.zip
Adding upstream version 0.8.0.upstream/0.8.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
-rw-r--r--.github/dependabot.yml22
-rw-r--r--.github/workflows/build.yml30
-rw-r--r--LICENSE21
-rw-r--r--README.md2
-rw-r--r--cjson/canonicaljson.go151
-rw-r--r--cjson/canonicaljson_test.go142
-rw-r--r--dsse/envelope.go64
-rw-r--r--dsse/sign.go85
-rw-r--r--dsse/sign_test.go380
-rw-r--r--dsse/signerverifier.go43
-rw-r--r--dsse/verify.go138
-rw-r--r--dsse/verify_test.go382
-rw-r--r--encrypted/encrypted.go290
-rw-r--r--encrypted/encrypted_test.go150
-rw-r--r--go.mod16
-rw-r--r--go.sum17
-rw-r--r--signerverifier/ecdsa.go111
-rw-r--r--signerverifier/ecdsa_test.go191
-rw-r--r--signerverifier/ed25519.go98
-rw-r--r--signerverifier/ed25519_test.go207
-rw-r--r--signerverifier/rsa.go161
-rw-r--r--signerverifier/rsa_test.go193
-rw-r--r--signerverifier/signerverifier.go36
-rw-r--r--signerverifier/test-data/ecdsa-test-key1
-rwxr-xr-xsignerverifier/test-data/ecdsa-test-key.pub1
-rw-r--r--signerverifier/test-data/ed25519-test-key1
-rw-r--r--signerverifier/test-data/ed25519-test-key.pub1
-rw-r--r--signerverifier/test-data/rsa-test-key39
-rw-r--r--signerverifier/test-data/rsa-test-key.pub11
-rw-r--r--signerverifier/test-data/test-ecdsa.98adf386.link17
-rw-r--r--signerverifier/test-data/test-ed25519.52e3b8e7.link17
-rw-r--r--signerverifier/test-data/test-rsa.4e8d20af.link17
-rw-r--r--signerverifier/utils.go149
-rw-r--r--signerverifier/utils_test.go41
34 files changed, 3225 insertions, 0 deletions
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..496998d
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,22 @@
+version: 2
+updates:
+ # Monitor Go dependencies
+ - package-ecosystem: "gomod"
+ directory: "/"
+ schedule:
+ interval: "daily"
+ time: "10:00"
+ commit-message:
+ prefix: "chore"
+ include: "scope"
+ open-pull-requests-limit: 10
+ # Monitor Github Actions
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "daily"
+ time: "10:00"
+ commit-message:
+ prefix: "chore"
+ include: "scope"
+ open-pull-requests-limit: 10
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..5f4dbae
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,30 @@
+on: [push, pull_request]
+name: build
+jobs:
+ test:
+ strategy:
+ matrix:
+ go-version: [1.18.x, 1.19.x, 1.20.x, 1.21.x]
+ os: [ubuntu-latest, macos-latest, windows-latest]
+ runs-on: ${{ matrix.os }}
+ steps:
+ - name: Install Go
+ uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491
+ with:
+ go-version: ${{ matrix.go-version }}
+ - name: Checkout code
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
+ - name: Format Unix
+ if: runner.os == 'Linux'
+ run: test -z $(go fmt ./...)
+ - name: Test
+ run: go test -v ./...
+ staticcheck:
+ name: "Run staticcheck"
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
+ - uses: dominikh/staticcheck-action@ba605356b4b29a60e87ab9404b712f3461e566dc
+ with:
+ version: "2022.1"
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..e51324f
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2021 NYU Secure Systems Lab
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..f632c63
--- /dev/null
+++ b/README.md
@@ -0,0 +1,2 @@
+A library that provides cryptographic and general-purpose functions for Go
+Secure Systems Lab projects at NYU.
diff --git a/cjson/canonicaljson.go b/cjson/canonicaljson.go
new file mode 100644
index 0000000..abc860a
--- /dev/null
+++ b/cjson/canonicaljson.go
@@ -0,0 +1,151 @@
+package cjson
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "reflect"
+ "sort"
+ "strings"
+)
+
+/*
+encodeCanonicalString is a helper function to canonicalize the passed string
+according to the OLPC canonical JSON specification for strings (see
+http://wiki.laptop.org/go/Canonical_JSON). String canonicalization consists of
+escaping backslashes ("\") and double quotes (") and wrapping the resulting
+string in double quotes (").
+*/
+func encodeCanonicalString(s string) string {
+ // Escape backslashes
+ s = strings.ReplaceAll(s, "\\", "\\\\")
+ // Escape double quotes
+ s = strings.ReplaceAll(s, "\"", "\\\"")
+ // Wrap with double quotes
+ return fmt.Sprintf("\"%s\"", s)
+}
+
+/*
+encodeCanonical is a helper function to recursively canonicalize the passed
+object according to the OLPC canonical JSON specification (see
+http://wiki.laptop.org/go/Canonical_JSON) and write it to the passed
+*bytes.Buffer. If canonicalization fails it returns an error.
+*/
+func encodeCanonical(obj interface{}, result *strings.Builder) (err error) {
+ switch objAsserted := obj.(type) {
+ case string:
+ result.WriteString(encodeCanonicalString(objAsserted))
+
+ case bool:
+ if objAsserted {
+ result.WriteString("true")
+ } else {
+ result.WriteString("false")
+ }
+
+ // The wrapping `EncodeCanonical` function decodes the passed json data with
+ // `decoder.UseNumber` so that any numeric value is stored as `json.Number`
+ // (instead of the default `float64`). This allows us to assert that it is a
+ // non-floating point number, which are the only numbers allowed by the used
+ // canonicalization specification.
+ case json.Number:
+ if _, err := objAsserted.Int64(); err != nil {
+ panic(fmt.Sprintf("Can't canonicalize floating point number '%s'",
+ objAsserted))
+ }
+ result.WriteString(objAsserted.String())
+
+ case nil:
+ result.WriteString("null")
+
+ // Canonicalize slice
+ case []interface{}:
+ result.WriteString("[")
+ for i, val := range objAsserted {
+ if err := encodeCanonical(val, result); err != nil {
+ return err
+ }
+ if i < (len(objAsserted) - 1) {
+ result.WriteString(",")
+ }
+ }
+ result.WriteString("]")
+
+ case map[string]interface{}:
+ result.WriteString("{")
+
+ // Make a list of keys
+ var mapKeys []string
+ for key := range objAsserted {
+ mapKeys = append(mapKeys, key)
+ }
+ // Sort keys
+ sort.Strings(mapKeys)
+
+ // Canonicalize map
+ for i, key := range mapKeys {
+ if err := encodeCanonical(key, result); err != nil {
+ return err
+ }
+
+ result.WriteString(":")
+ if err := encodeCanonical(objAsserted[key], result); err != nil {
+ return err
+ }
+ if i < (len(mapKeys) - 1) {
+ result.WriteString(",")
+ }
+ i++
+ }
+ result.WriteString("}")
+
+ default:
+ // We recover in a deferred function defined above
+ panic(fmt.Sprintf("Can't canonicalize '%s' of type '%s'",
+ objAsserted, reflect.TypeOf(objAsserted)))
+ }
+ return nil
+}
+
+/*
+EncodeCanonical JSON canonicalizes the passed object and returns it as a byte
+slice. It uses the OLPC canonical JSON specification (see
+http://wiki.laptop.org/go/Canonical_JSON). If canonicalization fails the byte
+slice is nil and the second return value contains the error.
+*/
+func EncodeCanonical(obj interface{}) (out []byte, err error) {
+ // We use panic if an error occurs and recover in a deferred function,
+ // which is always called before returning.
+ // There we set the error that is returned eventually.
+ defer func() {
+ if r := recover(); r != nil {
+ err = errors.New(r.(string))
+ }
+ }()
+
+ // FIXME: Terrible hack to turn the passed struct into a map, converting
+ // the struct's variable names to the json key names defined in the struct
+ data, err := json.Marshal(obj)
+ if err != nil {
+ return nil, err
+ }
+ var jsonMap interface{}
+
+ dec := json.NewDecoder(bytes.NewReader(data))
+ dec.UseNumber()
+ if err := dec.Decode(&jsonMap); err != nil {
+ return nil, err
+ }
+
+ // Create a buffer and write the canonicalized JSON bytes to it
+ var result strings.Builder
+ // Allocate output result buffer with the input size.
+ result.Grow(len(data))
+ // Recursively encode the jsonmap
+ if err := encodeCanonical(jsonMap, &result); err != nil {
+ return nil, err
+ }
+
+ return []byte(result.String()), nil
+}
diff --git a/cjson/canonicaljson_test.go b/cjson/canonicaljson_test.go
new file mode 100644
index 0000000..9449150
--- /dev/null
+++ b/cjson/canonicaljson_test.go
@@ -0,0 +1,142 @@
+package cjson
+
+import (
+ "encoding/json"
+ "fmt"
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+type keyVal struct {
+ Private string `json:"private"`
+ Public string `json:"public"`
+ Certificate string `json:"certificate,omitempty"`
+}
+
+type key struct {
+ KeyID string `json:"keyid"`
+ KeyIDHashAlgorithms []string `json:"keyid_hash_algorithms"`
+ KeyType string `json:"keytype"`
+ KeyVal keyVal `json:"keyval"`
+ Scheme string `json:"scheme"`
+}
+
+func TestEncodeCanonical(t *testing.T) {
+ objects := []interface{}{
+ key{},
+ key{
+ KeyVal: keyVal{
+ Private: "priv",
+ Public: "pub",
+ },
+ KeyIDHashAlgorithms: []string{"hash"},
+ KeyID: "id",
+ KeyType: "type",
+ Scheme: "scheme",
+ },
+ map[string]interface{}{
+ "true": true,
+ "false": false,
+ "nil": nil,
+ "int": 3,
+ "int2": float64(42),
+ "string": `\"`,
+ },
+ key{
+ KeyVal: keyVal{
+ Certificate: "cert",
+ Private: "priv",
+ Public: "pub",
+ },
+ KeyIDHashAlgorithms: []string{"hash"},
+ KeyID: "id",
+ KeyType: "type",
+ Scheme: "scheme",
+ },
+ json.RawMessage(`{"_type":"targets","spec_version":"1.0","version":0,"expires":"0001-01-01T00:00:00Z","targets":{},"custom":{"test":true}}`),
+ }
+ expectedResult := []string{
+ `{"keyid":"","keyid_hash_algorithms":null,"keytype":"","keyval":{"private":"","public":""},"scheme":""}`,
+ `{"keyid":"id","keyid_hash_algorithms":["hash"],"keytype":"type","keyval":{"private":"priv","public":"pub"},"scheme":"scheme"}`,
+ `{"false":false,"int":3,"int2":42,"nil":null,"string":"\\\"","true":true}`,
+ `{"keyid":"id","keyid_hash_algorithms":["hash"],"keytype":"type","keyval":{"certificate":"cert","private":"priv","public":"pub"},"scheme":"scheme"}`,
+ `{"_type":"targets","custom":{"test":true},"expires":"0001-01-01T00:00:00Z","spec_version":"1.0","targets":{},"version":0}`,
+ }
+ for i := 0; i < len(objects); i++ {
+ result, err := EncodeCanonical(objects[i])
+
+ if string(result) != expectedResult[i] || err != nil {
+ t.Errorf("EncodeCanonical returned (%s, %s), expected (%s, nil)",
+ result, err, expectedResult[i])
+ }
+ }
+}
+
+func TestEncodeCanonicalErr(t *testing.T) {
+ objects := []interface{}{
+ map[string]interface{}{"float": 3.14159265359},
+ TestEncodeCanonical,
+ }
+ errPart := []string{
+ "Can't canonicalize floating point number",
+ "unsupported type: func(",
+ }
+
+ for i := 0; i < len(objects); i++ {
+ result, err := EncodeCanonical(objects[i])
+ if err == nil || !strings.Contains(err.Error(), errPart[i]) {
+ t.Errorf("EncodeCanonical returned (%s, %s), expected '%s' error",
+ result, err, errPart[i])
+ }
+ }
+}
+
+func TestEncodeCanonicalHelper(t *testing.T) {
+ defer func() {
+ if r := recover(); r == nil {
+ t.Error("encodeCanonical did not panic as expected")
+ }
+ }()
+
+ objects := []interface{}{
+ TestEncodeCanonicalHelper,
+ []interface{}{TestEncodeCanonicalHelper},
+ }
+
+ for i := 0; i < len(objects); i++ {
+ var result strings.Builder
+ err := encodeCanonical(objects[i], &result)
+ assert.Nil(t, err)
+ }
+}
+
+// -----------------------------------------------------------------------------
+
+// Size 146b
+var smallFixture = json.RawMessage(`{"keyid":"id","keyid_hash_algorithms":["hash"],"keytype":"type","keyval":{"certificate":"cert","private":"priv","public":"pub"},"scheme":"scheme"}`)
+
+// Response from Github Webhook. Size: 2.7kb
+var mediumFixture = json.RawMessage(`{"after":"1481a2de7b2a7d02428ad93446ab166be7793fbb","before":"17c497ccc7cca9c2f735aa07e9e3813060ce9a6a","commits":[{"added":[],"author":{"email":"lolwut@noway.biz","name":"Garen Torikian","username":"octokitty"},"committer":{"email":"lolwut@noway.biz","name":"Garen Torikian","username":"octokitty"},"distinct":true,"id":"c441029cf673f84c8b7db52d0a5944ee5c52ff89","message":"Test","modified":["README.md"],"removed":[],"timestamp":"2013-02-22T13:50:07-08:00","url":"https://github.com/octokitty/testing/commit/c441029cf673f84c8b7db52d0a5944ee5c52ff89"},{"added":[],"author":{"email":"lolwut@noway.biz","name":"Garen Torikian","username":"octokitty"},"committer":{"email":"lolwut@noway.biz","name":"Garen Torikian","username":"octokitty"},"distinct":true,"id":"36c5f2243ed24de58284a96f2a643bed8c028658","message":"This is me testing the windows client.","modified":["README.md"],"removed":[],"timestamp":"2013-02-22T14:07:13-08:00","url":"https://github.com/octokitty/testing/commit/36c5f2243ed24de58284a96f2a643bed8c028658"},{"added":["words/madame-bovary.txt"],"author":{"email":"lolwut@noway.biz","name":"Garen Torikian","username":"octokitty"},"committer":{"email":"lolwut@noway.biz","name":"Garen Torikian","username":"octokitty"},"distinct":true,"id":"1481a2de7b2a7d02428ad93446ab166be7793fbb","message":"Rename madame-bovary.txt to words/madame-bovary.txt","modified":[],"removed":["madame-bovary.txt"],"timestamp":"2013-03-12T08:14:29-07:00","url":"https://github.com/octokitty/testing/commit/1481a2de7b2a7d02428ad93446ab166be7793fbb"}],"compare":"https://github.com/octokitty/testing/compare/17c497ccc7cc...1481a2de7b2a","created":false,"deleted":false,"forced":false,"head_commit":{"added":["words/madame-bovary.txt"],"author":{"email":"lolwut@noway.biz","name":"Garen Torikian","username":"octokitty"},"committer":{"email":"lolwut@noway.biz","name":"Garen Torikian","username":"octokitty"},"distinct":true,"id":"1481a2de7b2a7d02428ad93446ab166be7793fbb","message":"Rename madame-bovary.txt to words/madame-bovary.txt","modified":[],"removed":["madame-bovary.txt"],"timestamp":"2013-03-12T08:14:29-07:00","url":"https://github.com/octokitty/testing/commit/1481a2de7b2a7d02428ad93446ab166be7793fbb"},"pusher":{"email":"lolwut@noway.biz","name":"Garen Torikian"},"ref":"refs/heads/master","repository":{"created_at":1332977768,"description":"","fork":false,"forks":0,"has_downloads":true,"has_issues":true,"has_wiki":true,"homepage":"","id":3860742,"language":"Ruby","master_branch":"master","name":"testing","open_issues":2,"owner":{"email":"lolwut@noway.biz","name":"octokitty"},"private":false,"pushed_at":1363295520,"size":2156,"stargazers":1,"url":"https://github.com/octokitty/testing","watchers":1}}`)
+
+// Response from Facebook. Size: 6.5kb
+var largeFixture = json.RawMessage(`{"stat":"ok","profile":{"providerName":"Facebook","identifier":"http://www.facebook.com/profile.php?id=100BlahBlah7767","verifiedEmail":"2013-11-22 21:01:09.601637 +0000","preferredUsername":"RpxDoc","displayName":"Rpx Doc","name":{"formatted":"Rpx Doc","givenName":"Rpx","familyName":"Doc"},"email":"rpxdoc@yahoo.com","url":"http://www.facebook.com/rpx.doc","photo":"https://graph.facebook.com/100BlahBlah7767/picture?type=large","utcOffset":"-08:00","address":{"formatted":"Portland, Oregon","type":"currentLocation"},"birthday":"1994-05-19","gender":"female"},"merged_poco":{"id":"http://www.facebook.com/profile.php?id=100BlahBlah7767","displayName":"Rpx Doc","preferredUsername":"RpxDoc","gender":"female","aboutMe":"i test stuff","profileUrl":"http://www.facebook.com/rpx.doc","relationshipStatus":"Widowed","religion":"u0627u0644u0627u0633u0644u0627u0645","status":"set_status timestamp test: Wed, 17 Oct 12 21:36:34 +0000","currentLocation":{"formatted":"Portland, Oregon"},"politicalViews":"Bude mu00edt toto vejce vu00edce fanouu0161ku016f neu017e Jiu0159u00ed Paroubek ?","name":{"formatted":"Rpx Doc","givenName":"Rpx","familyName":"Doc"},"updated":"2012-09-13T00:44:03.000Z","birthday":"1994-05-19","utcOffset":"-08:00","emails":[{"value":"rpxdoc@yahoo.com","type":"other","primary":true}],"languagesSpoken":["Pig Latin"],"urls":[{"value":"http://www.facebook.com/rpx.doc","type":"profile"},{"value":"http://www.facepalm.org","type":"other"},{"value":"http://foo.com","type":"other"}],"addresses":[{"formatted":"Portland, Oregon","type":"currentLocation"},{"formatted":"Luxembourg","type":"hometown"}],"books":["Dr. Seuss' The Cat in the Hat","Good Omens"],"movies":["Gigli","Big Trouble in Little China"],"music":["My favorite playlist","Country music","Western"],"tvShows":["Voltran","American Idol","ThunderCats","Seinfeld"],"quotes":["I'm getting ENOSPACE writing to /dev/null."],"interests":["Justin Bieber"],"sports":["Frolf","Underwater hockey"],"heroes":["Donkey","Shrek"],"activities":["Underwater basket weaving"],"photos":[{"value":"https://graph.facebook.com/100BlahBlah7767/picture?type=small","type":"other"},{"value":"https://graph.facebook.com/100BlahBlah7767/picture?type=large","type":"other","primary":true},{"value":"https://graph.facebook.com/100BlahBlah7767/picture?type=square","type":"other"},{"value":"https://graph.facebook.com/100BlahBlah7767/picture?type=normal","type":"other"}],"organizations":[{"name":"Janrain","title":"Tester","type":"job","startDate":"2007-05","description":"I am."},{"name":"Janrain","title":"a wee tester","type":"job","startDate":"0000-00","description":"something clever"},{"name":"Janrain","title":"To Test","type":"job","startDate":"2009-01","endDate":"2009-02"},{"name":"Janrain","title":"Testing Monkey","type":"job","startDate":"2006-02","endDate":"2005-02","description":"I was."},{"name":"School Of Rock","type":"High School"},{"name":"Hogwarts School of Witchcraft and Wizardry","type":"College"}]},"friends":["http://www.facebook.com/profile.php?id=1234BlahBlah254","http://www.facebook.com/profile.php?id=1234BlahBlah434","http://www.facebook.com/profile.php?id=1234BlahBlah662"],"provider":{"facebook":{"albums":[{"id":"326BlahBlah6808","name":"Untitled Album","link":"http://www.facebook.com/album.php?fbid=1234BlahBlah808&id=100BlahBlah7767&aid=78839","privacy":"custom","type":"normal"},{"id":"326BlahBlah0163","name":"Timeline Photos","link":"http://www.facebook.com/album.php?fbid=326BlahBlah0163&id=100BlahBlah7767&aid=78838","privacy":"everyone","type":"wall"},{"id":"322BlahBlah7306","name":"Cover Photos","link":"http://www.facebook.com/album.php?fbid=322BlahBlah7306&id=100BlahBlah7767&aid=77860","privacy":"everyone","type":"normal"},{"id":"322BlahBlah1017","name":"Untitled Album","link":"http://www.facebook.com/album.php?fbid=322BlahBlah1017&id=100BlahBlah7767&aid=77858","privacy":"custom","type":"normal"},{"id":"102BlahBlah3100","name":"Profile Pictures","link":"http://www.facebook.com/album.php?fbid=102BlahBlah3100&id=100BlahBlah7767&aid=4035","privacy":"everyone","type":"profile"}],"games":[{"name":"Axis & Allies","category":"Interest","id":"124BlahBlah6166"},{"name":"UNO","category":"Games/toys","id":"123BlahBlah6939"}],"groups":[{"name":"Test group","id":"123BlahBlah2994"},{"name":"Exploratory Group","id":"123BlahBlah7259"}],"videos":[{"id":"350BlahBlah1104","description":"a super awesome movie!!!","picture":"http://example.com/hvthumb-ak-snc6/245400_350BlahBlah1061_350BlahBlah1104_2773_417_t.jpg","icon":"http://example.com/rsrc.php/v2/yD/r/DggBlahz4tO.gif","embed_html":"","source":"http://example.com/cfs-ak-ash4/v/34xyz3/743/350BlahBlah1104_8269.mp4?oh=3f74c5a67BlahBlah33eb2d7f72d0dc1&oe=5080CF78&__gda__=1350674533_97d8568b1a07387e4cee5d02d87262b9"},{"id":"123BlahBlah7762","description":"what what!","picture":"http://example.com/hvthumb-ak-ash4/245318_350BlahBlah4397_350BlahBlah7762_37327_361_t.jpg","icon":"http://example.com/rsrc.php/v2/yD/r/DggBlahz4tO.gif","embed_html":"","source":"http://example.com/cfs-ak-snc7/v/610161/125/350BlahBlah7762_24214.mp4?oh=3f527BlahBlahBlahBlah8dd9c665ba0&oe=5080F026&__gda__=1350Blah08_f3da7404BlahBlah6f886b3fce52ea4a"}]}},"limited_data":"false","accessCredentials":{"accessToken":"AAAFArLqJQIBlahBlaha0rCdu9m5d5fBlahBlahFKYWpp401H9LGf5rQasuZAzrMyoZA9J45FDSZACLyNCXkAZAgpDFr0hG8NBkb8CccXXuQZDZD","uid":"100BlahBlah7767","expires":1355690751,"scopes":"email,publish_stream,user_birthday,user_location,user_hometown,user_relationships,user_interests,user_about_me,user_photos,user_work_history,friends_hometown,friends_interests,friends_relationships,friends_photos,friends_location,friends_about_me,friends_birthday,friends_work_history,read_stream,read_insights,create_event,rsvp_event,sms,read_requests,read_mailbox,read_friendlists,xmpp_login,ads_management,manage_pages,user_checkins,friends_checkins,publish_checkins,user_online_presence,friends_online_presence,user_education_history,friends_education_history,user_religion_politics,friends_religion_politics,user_likes,manage_notifications,friends_actions.music,user_actions.music,user_activities,friends_likes,friends_relationship_details,publish_actions,friends_events,user_notes,friends_notes,friends_questions,friends_videos,user_website,friends_status,friends_activities,manage_friendlists,user_events,user_groups,friends_groups,user_questions,user_videos,friends_website","type":"Facebook"}}`)
+
+func BenchmarkEncodeCanonical(b *testing.B) {
+ var table = []struct {
+ input json.RawMessage
+ }{
+ {input: smallFixture},
+ {input: mediumFixture},
+ {input: largeFixture},
+ }
+
+ for _, v := range table {
+ b.Run(fmt.Sprintf("input_size_%d", len(v.input)), func(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ EncodeCanonical(v.input)
+ }
+ })
+ }
+}
diff --git a/dsse/envelope.go b/dsse/envelope.go
new file mode 100644
index 0000000..ed223e9
--- /dev/null
+++ b/dsse/envelope.go
@@ -0,0 +1,64 @@
+package dsse
+
+import (
+ "encoding/base64"
+ "fmt"
+)
+
+/*
+Envelope captures an envelope as described by the DSSE specification. See here:
+https://github.com/secure-systems-lab/dsse/blob/master/envelope.md
+*/
+type Envelope struct {
+ PayloadType string `json:"payloadType"`
+ Payload string `json:"payload"`
+ Signatures []Signature `json:"signatures"`
+}
+
+/*
+DecodeB64Payload returns the serialized body, decoded from the envelope's
+payload field. A flexible decoder is used, first trying standard base64, then
+URL-encoded base64.
+*/
+func (e *Envelope) DecodeB64Payload() ([]byte, error) {
+ return b64Decode(e.Payload)
+}
+
+/*
+Signature represents a generic in-toto signature that contains the identifier
+of the key which was used to create the signature.
+The used signature scheme has to be agreed upon by the signer and verifer
+out of band.
+The signature is a base64 encoding of the raw bytes from the signature
+algorithm.
+*/
+type Signature struct {
+ KeyID string `json:"keyid"`
+ Sig string `json:"sig"`
+}
+
+/*
+PAE implementes the DSSE Pre-Authentic Encoding
+https://github.com/secure-systems-lab/dsse/blob/master/protocol.md#signature-definition
+*/
+func PAE(payloadType string, payload []byte) []byte {
+ return []byte(fmt.Sprintf("DSSEv1 %d %s %d %s",
+ len(payloadType), payloadType,
+ len(payload), payload))
+}
+
+/*
+Both standard and url encoding are allowed:
+https://github.com/secure-systems-lab/dsse/blob/master/envelope.md
+*/
+func b64Decode(s string) ([]byte, error) {
+ b, err := base64.StdEncoding.DecodeString(s)
+ if err != nil {
+ b, err = base64.URLEncoding.DecodeString(s)
+ if err != nil {
+ return nil, fmt.Errorf("unable to base64 decode payload (is payload in the right format?)")
+ }
+ }
+
+ return b, nil
+}
diff --git a/dsse/sign.go b/dsse/sign.go
new file mode 100644
index 0000000..244a806
--- /dev/null
+++ b/dsse/sign.go
@@ -0,0 +1,85 @@
+/*
+Package dsse implements the Dead Simple Signing Envelope (DSSE)
+https://github.com/secure-systems-lab/dsse
+*/
+package dsse
+
+import (
+ "context"
+ "encoding/base64"
+ "errors"
+)
+
+// ErrNoSigners indicates that no signer was provided.
+var ErrNoSigners = errors.New("no signers provided")
+
+// EnvelopeSigner creates signed Envelopes.
+type EnvelopeSigner struct {
+ providers []Signer
+}
+
+/*
+NewEnvelopeSigner creates an EnvelopeSigner that uses 1+ Signer algorithms to
+sign the data.
+*/
+func NewEnvelopeSigner(p ...Signer) (*EnvelopeSigner, error) {
+ var providers []Signer
+
+ for _, s := range p {
+ if s != nil {
+ providers = append(providers, s)
+ }
+ }
+
+ if len(providers) == 0 {
+ return nil, ErrNoSigners
+ }
+
+ return &EnvelopeSigner{
+ providers: providers,
+ }, nil
+}
+
+/*
+NewMultiEnvelopeSigner creates an EnvelopeSigner that uses 1+ Signer
+algorithms to sign the data. The threshold parameter is legacy and is ignored.
+
+Deprecated: This function simply calls NewEnvelopeSigner, and that function should
+be preferred.
+*/
+func NewMultiEnvelopeSigner(threshold int, p ...Signer) (*EnvelopeSigner, error) {
+ return NewEnvelopeSigner(p...)
+}
+
+/*
+SignPayload signs a payload and payload type according to DSSE.
+Returned is an envelope as defined here:
+https://github.com/secure-systems-lab/dsse/blob/master/envelope.md
+One signature will be added for each Signer in the EnvelopeSigner.
+*/
+func (es *EnvelopeSigner) SignPayload(ctx context.Context, payloadType string, body []byte) (*Envelope, error) {
+ var e = Envelope{
+ Payload: base64.StdEncoding.EncodeToString(body),
+ PayloadType: payloadType,
+ }
+
+ paeEnc := PAE(payloadType, body)
+
+ for _, signer := range es.providers {
+ sig, err := signer.Sign(ctx, paeEnc)
+ if err != nil {
+ return nil, err
+ }
+ keyID, err := signer.KeyID()
+ if err != nil {
+ keyID = ""
+ }
+
+ e.Signatures = append(e.Signatures, Signature{
+ KeyID: keyID,
+ Sig: base64.StdEncoding.EncodeToString(sig),
+ })
+ }
+
+ return &e, nil
+}
diff --git a/dsse/sign_test.go b/dsse/sign_test.go
new file mode 100644
index 0000000..c4abb73
--- /dev/null
+++ b/dsse/sign_test.go
@@ -0,0 +1,380 @@
+package dsse
+
+import (
+ "context"
+ "crypto"
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "crypto/sha256"
+ "encoding/base64"
+ "errors"
+ "fmt"
+ "math/big"
+ "testing"
+
+ "github.com/codahale/rfc6979"
+ "github.com/stretchr/testify/assert"
+)
+
+var errLength = errors.New("invalid length")
+
+func TestPAE(t *testing.T) {
+ t.Run("Empty", func(t *testing.T) {
+ var want = []byte("DSSEv1 0 0 ")
+
+ got := PAE("", []byte{})
+ assert.Equal(t, want, got, "Wrong encoding")
+ })
+ t.Run("Hello world", func(t *testing.T) {
+ var want = []byte("DSSEv1 29 http://example.com/HelloWorld 11 hello world")
+
+ got := PAE("http://example.com/HelloWorld", []byte("hello world"))
+ assert.Equal(t, want, got, "Wrong encoding")
+ })
+ t.Run("Unicode-only", func(t *testing.T) {
+ var want = []byte("DSSEv1 29 http://example.com/HelloWorld 3 ಠ")
+
+ got := PAE("http://example.com/HelloWorld", []byte("ಠ"))
+ assert.Equal(t, want, got, "Wrong encoding")
+ })
+}
+
+type nilSignerVerifier int
+
+func (n nilSignerVerifier) Sign(ctx context.Context, data []byte) ([]byte, error) {
+ return data, nil
+}
+
+func (n nilSignerVerifier) Verify(ctx context.Context, data, sig []byte) error {
+ if len(data) != len(sig) {
+ return errLength
+ }
+
+ for i := range data {
+ if data[i] != sig[i] {
+ return errVerify
+ }
+ }
+
+ return nil
+}
+
+func (n nilSignerVerifier) KeyID() (string, error) {
+ return "nil", nil
+}
+
+func (n nilSignerVerifier) Public() crypto.PublicKey {
+ return "nil-public"
+}
+
+type nullSignerVerifier int
+
+func (n nullSignerVerifier) Sign(ctx context.Context, data []byte) ([]byte, error) {
+ return data, nil
+}
+
+func (n nullSignerVerifier) Verify(ctx context.Context, data, sig []byte) error {
+ if len(data) != len(sig) {
+ return errLength
+ }
+
+ for i := range data {
+ if data[i] != sig[i] {
+ return errVerify
+ }
+ }
+
+ return nil
+}
+
+func (n nullSignerVerifier) KeyID() (string, error) {
+ return "null", nil
+}
+
+func (n nullSignerVerifier) Public() crypto.PublicKey {
+ return "null-public"
+}
+
+type errsigner int
+
+func (n errsigner) Sign(ctx context.Context, data []byte) ([]byte, error) {
+ return nil, fmt.Errorf("signing error")
+}
+
+func (n errsigner) Verify(ctx context.Context, data, sig []byte) error {
+ return errVerify
+}
+
+func (n errsigner) KeyID() (string, error) {
+ return "err", nil
+}
+
+func (n errsigner) Public() crypto.PublicKey {
+ return "err-public"
+}
+
+type errSignerVerifier int
+
+var errVerify = fmt.Errorf("accepted signatures do not match threshold, Found: 0, Expected 1")
+var errThreshold = fmt.Errorf("invalid threshold")
+
+func (n errSignerVerifier) Sign(ctx context.Context, data []byte) ([]byte, error) {
+ return data, nil
+}
+
+func (n errSignerVerifier) Verify(ctx context.Context, data, sig []byte) error {
+ return errVerify
+}
+
+func (n errSignerVerifier) KeyID() (string, error) {
+ return "err", nil
+}
+
+func (n errSignerVerifier) Public() crypto.PublicKey {
+ return "err-public"
+}
+
+type badverifier int
+
+func (n badverifier) Sign(ctx context.Context, data []byte) ([]byte, error) {
+ return append(data, byte(0)), nil
+}
+
+func (n badverifier) Verify(ctx context.Context, data, sig []byte) error {
+
+ if len(data) != len(sig) {
+ return errLength
+ }
+
+ for i := range data {
+ if data[i] != sig[i] {
+ return errVerify
+ }
+ }
+
+ return nil
+}
+
+func (n badverifier) KeyID() (string, error) {
+ return "bad", nil
+}
+
+func (n badverifier) Public() crypto.PublicKey {
+ return "bad-public"
+}
+
+func TestNoSigners(t *testing.T) {
+ t.Run("nil slice", func(t *testing.T) {
+ signer, err := NewEnvelopeSigner(nil)
+ assert.Nil(t, signer, "unexpected signer")
+ assert.NotNil(t, err, "error expected")
+ assert.Equal(t, ErrNoSigners, err, "wrong error")
+ })
+
+ t.Run("empty slice", func(t *testing.T) {
+ signer, err := NewEnvelopeSigner([]Signer{}...)
+ assert.Nil(t, signer, "unexpected signer")
+ assert.NotNil(t, err, "error expected")
+ assert.Equal(t, ErrNoSigners, err, "wrong error")
+ })
+}
+
+func TestNilSign(t *testing.T) {
+ var keyID = "nil"
+ var payloadType = "http://example.com/HelloWorld"
+ var payload = []byte("hello world")
+
+ pae := PAE(payloadType, payload)
+ want := Envelope{
+ Payload: base64.StdEncoding.EncodeToString([]byte(payload)),
+ PayloadType: payloadType,
+ Signatures: []Signature{
+ {
+ KeyID: keyID,
+ Sig: base64.StdEncoding.EncodeToString(pae),
+ },
+ },
+ }
+
+ var ns nilSignerVerifier
+ signer, err := NewEnvelopeSigner(ns)
+ assert.Nil(t, err, "unexpected error")
+
+ got, err := signer.SignPayload(context.TODO(), payloadType, []byte(payload))
+ assert.Nil(t, err, "sign failed")
+ assert.Equal(t, &want, got, "bad signature")
+}
+
+func TestSignError(t *testing.T) {
+ var es errsigner
+ signer, err := NewEnvelopeSigner(es)
+ assert.Nil(t, err, "unexpected error")
+
+ got, err := signer.SignPayload(context.TODO(), "t", []byte("d"))
+ assert.Nil(t, got, "expected nil")
+ assert.NotNil(t, err, "error expected")
+ assert.Equal(t, "signing error", err.Error(), "wrong error")
+}
+
+func newEcdsaKey() *ecdsa.PrivateKey {
+ var x big.Int
+ var y big.Int
+ var d big.Int
+
+ _, ok := x.SetString("46950820868899156662930047687818585632848591499744589407958293238635476079160", 10)
+ if !ok {
+ return nil
+ }
+ _, ok = y.SetString("5640078356564379163099075877009565129882514886557779369047442380624545832820", 10)
+ if !ok {
+ return nil
+ }
+ _, ok = d.SetString("97358161215184420915383655311931858321456579547487070936769975997791359926199", 10)
+ if !ok {
+ return nil
+ }
+
+ var private = ecdsa.PrivateKey{
+ PublicKey: ecdsa.PublicKey{
+ Curve: elliptic.P256(),
+ X: &x,
+ Y: &y,
+ },
+ D: &d,
+ }
+
+ return &private
+}
+
+type ecdsaSignerVerifier struct {
+ keyID string
+ key *ecdsa.PrivateKey
+ rLen int
+ verified bool
+}
+
+func (es *ecdsaSignerVerifier) Sign(ctx context.Context, data []byte) ([]byte, error) {
+ // Data is complete message, hash it and sign the digest
+ digest := sha256.Sum256(data)
+ r, s, err := rfc6979.SignECDSA(es.key, digest[:], sha256.New)
+ if err != nil {
+ return nil, err
+ }
+
+ rb := r.Bytes()
+ sb := s.Bytes()
+ es.rLen = len(rb)
+ rawSig := append(rb, sb...)
+
+ return rawSig, nil
+}
+
+func (es *ecdsaSignerVerifier) Verify(ctx context.Context, data, sig []byte) error {
+ var r big.Int
+ var s big.Int
+ digest := sha256.Sum256(data)
+ // Signature here is the raw bytes of r and s concatenated
+ rb := sig[:es.rLen]
+ sb := sig[es.rLen:]
+ r.SetBytes(rb)
+ s.SetBytes(sb)
+
+ ok := ecdsa.Verify(&es.key.PublicKey, digest[:], &r, &s)
+ es.verified = ok
+
+ if ok {
+ return nil
+ }
+ return errVerify
+}
+
+func (es *ecdsaSignerVerifier) KeyID() (string, error) {
+ return es.keyID, nil
+}
+
+func (es *ecdsaSignerVerifier) Public() crypto.PublicKey {
+ return es.key.Public()
+}
+
+// Test against the example in the protocol specification:
+// https://github.com/secure-systems-lab/dsse/blob/master/protocol.md
+func TestEcdsaSign(t *testing.T) {
+ var keyID = "test key 123"
+ var payloadType = "http://example.com/HelloWorld"
+ var payload = "hello world"
+ var ecdsa = &ecdsaSignerVerifier{
+ keyID: keyID,
+ key: newEcdsaKey(),
+ }
+ var want = Envelope{
+ Payload: "aGVsbG8gd29ybGQ=",
+ PayloadType: payloadType,
+ Signatures: []Signature{
+ {
+ KeyID: keyID,
+ Sig: "A3JqsQGtVsJ2O2xqrI5IcnXip5GToJ3F+FnZ+O88SjtR6rDAajabZKciJTfUiHqJPcIAriEGAHTVeCUjW2JIZA==",
+ },
+ },
+ }
+
+ signer, err := NewEnvelopeSigner(ecdsa)
+ assert.Nil(t, err, "unexpected error")
+
+ env, err := signer.SignPayload(context.TODO(), payloadType, []byte(payload))
+ assert.Nil(t, err, "unexpected error")
+ assert.Equal(t, &want, env, "Wrong envelope generated")
+
+ // Now verify
+ verifier, err := NewEnvelopeVerifier(ecdsa)
+ assert.Nil(t, err, "unexpected error")
+ acceptedKeys, err := verifier.Verify(context.TODO(), env)
+ assert.Nil(t, err, "unexpected error")
+ assert.True(t, ecdsa.verified, "verify was not called")
+ assert.Len(t, acceptedKeys, 1, "unexpected keys")
+ assert.Equal(t, acceptedKeys[0].KeyID, keyID, "unexpected keyid")
+}
+
+func TestDecodeB64Payload(t *testing.T) {
+ var want = make([]byte, 256)
+ for i := range want {
+ want[i] = byte(i)
+ }
+ var b64Url = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn+AgYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq+wsbKztLW2t7i5uru8vb6/wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t/g4eLj5OXm5+jp6uvs7e7v8PHy8/T19vf4+fr7/P3+/w=="
+ var b64Std = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0-P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn-AgYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq-wsbKztLW2t7i5uru8vb6_wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t_g4eLj5OXm5-jp6uvs7e7v8PHy8_T19vf4-fr7_P3-_w=="
+ var b64UrlErr = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn+AgYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq+wsbKztLW2t7i5uru8vb6/wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t/g4eLj5OXm5+jp6uvs7e7v8PHy8/T19vf4+fr7/P3+/w"
+ var b64StdErr = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0-P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn-AgYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq-wsbKztLW2t7i5uru8vb6_wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t_g4eLj5OXm5-jp6uvs7e7v8PHy8_T19vf4-fr7_P3-_w"
+
+ t.Run("Standard encoding", func(t *testing.T) {
+ env := &Envelope{
+ Payload: b64Std,
+ }
+ got, err := env.DecodeB64Payload()
+ assert.Nil(t, err, "unexpected error")
+ assert.Equal(t, want, got, "wrong data")
+ })
+ t.Run("URL encoding", func(t *testing.T) {
+ env := &Envelope{
+ Payload: b64Url,
+ }
+ got, err := env.DecodeB64Payload()
+ assert.Nil(t, err, "unexpected error")
+ assert.Equal(t, want, got, "wrong data")
+ })
+
+ t.Run("Standard encoding - error", func(t *testing.T) {
+ env := &Envelope{
+ Payload: b64StdErr,
+ }
+ got, err := env.DecodeB64Payload()
+ assert.NotNil(t, err, "expected error")
+ assert.Nil(t, got, "wrong data")
+ })
+ t.Run("URL encoding - error", func(t *testing.T) {
+ env := &Envelope{
+ Payload: b64UrlErr,
+ }
+ got, err := env.DecodeB64Payload()
+ assert.NotNil(t, err, "expected error")
+ assert.Nil(t, got, "wrong data")
+ })
+}
diff --git a/dsse/signerverifier.go b/dsse/signerverifier.go
new file mode 100644
index 0000000..99d03c7
--- /dev/null
+++ b/dsse/signerverifier.go
@@ -0,0 +1,43 @@
+package dsse
+
+import (
+ "context"
+ "crypto"
+)
+
+/*
+Signer defines the interface for an abstract signing algorithm. The Signer
+interface is used to inject signature algorithm implementations into the
+EnvelopeSigner. This decoupling allows for any signing algorithm and key
+management system can be used. The full message is provided as the parameter.
+If the signature algorithm depends on hashing of the message prior to signature
+calculation, the implementor of this interface must perform such hashing. The
+function must return raw bytes representing the calculated signature using the
+current algorithm, and the key used (if applicable).
+*/
+type Signer interface {
+ Sign(ctx context.Context, data []byte) ([]byte, error)
+ KeyID() (string, error)
+}
+
+/*
+Verifier verifies a complete message against a signature and key. If the message
+was hashed prior to signature generation, the verifier must perform the same
+steps. If KeyID returns successfully, only signature matching the key ID will be
+verified.
+*/
+type Verifier interface {
+ Verify(ctx context.Context, data, sig []byte) error
+ KeyID() (string, error)
+ Public() crypto.PublicKey
+}
+
+// SignerVerifier provides both the signing and verification interface.
+type SignerVerifier interface {
+ Signer
+ Verifier
+}
+
+// Deprecated: switch to renamed SignerVerifier. This is currently aliased for
+// backwards compatibility.
+type SignVerifier = SignerVerifier
diff --git a/dsse/verify.go b/dsse/verify.go
new file mode 100644
index 0000000..a36146b
--- /dev/null
+++ b/dsse/verify.go
@@ -0,0 +1,138 @@
+package dsse
+
+import (
+ "context"
+ "crypto"
+ "errors"
+ "fmt"
+
+ "golang.org/x/crypto/ssh"
+)
+
+// ErrNoSignature indicates that an envelope did not contain any signatures.
+var ErrNoSignature = errors.New("no signature found")
+
+type EnvelopeVerifier struct {
+ providers []Verifier
+ threshold int
+}
+
+type AcceptedKey struct {
+ Public crypto.PublicKey
+ KeyID string
+ Sig Signature
+}
+
+func (ev *EnvelopeVerifier) Verify(ctx context.Context, e *Envelope) ([]AcceptedKey, error) {
+ if e == nil {
+ return nil, errors.New("cannot verify a nil envelope")
+ }
+
+ if len(e.Signatures) == 0 {
+ return nil, ErrNoSignature
+ }
+
+ // Decode payload (i.e serialized body)
+ body, err := e.DecodeB64Payload()
+ if err != nil {
+ return nil, err
+ }
+ // Generate PAE(payloadtype, serialized body)
+ paeEnc := PAE(e.PayloadType, body)
+
+ // If *any* signature is found to be incorrect, it is skipped
+ var acceptedKeys []AcceptedKey
+ usedKeyids := make(map[string]string)
+ unverified_providers := ev.providers
+ for _, s := range e.Signatures {
+ sig, err := b64Decode(s.Sig)
+ if err != nil {
+ return nil, err
+ }
+
+ // Loop over the providers.
+ // If provider and signature include key IDs but do not match skip.
+ // If a provider recognizes the key, we exit
+ // the loop and use the result.
+ providers := unverified_providers
+ for i, v := range providers {
+ keyID, err := v.KeyID()
+
+ // Verifiers that do not provide a keyid will be generated one using public.
+ if err != nil || keyID == "" {
+ keyID, err = SHA256KeyID(v.Public())
+ if err != nil {
+ keyID = ""
+ }
+ }
+
+ if s.KeyID != "" && keyID != "" && err == nil && s.KeyID != keyID {
+ continue
+ }
+
+ err = v.Verify(ctx, paeEnc, sig)
+ if err != nil {
+ continue
+ }
+
+ acceptedKey := AcceptedKey{
+ Public: v.Public(),
+ KeyID: keyID,
+ Sig: s,
+ }
+ unverified_providers = removeIndex(providers, i)
+
+ // See https://github.com/in-toto/in-toto/pull/251
+ if _, ok := usedKeyids[keyID]; ok {
+ fmt.Printf("Found envelope signed by different subkeys of the same main key, Only one of them is counted towards the step threshold, KeyID=%s\n", keyID)
+ continue
+ }
+
+ usedKeyids[keyID] = ""
+ acceptedKeys = append(acceptedKeys, acceptedKey)
+ break
+ }
+ }
+
+ // Sanity if with some reflect magic this happens.
+ if ev.threshold <= 0 || ev.threshold > len(ev.providers) {
+ return nil, errors.New("invalid threshold")
+ }
+
+ if len(usedKeyids) < ev.threshold {
+ return acceptedKeys, fmt.Errorf("accepted signatures do not match threshold, Found: %d, Expected %d", len(acceptedKeys), ev.threshold)
+ }
+
+ return acceptedKeys, nil
+}
+
+func NewEnvelopeVerifier(v ...Verifier) (*EnvelopeVerifier, error) {
+ return NewMultiEnvelopeVerifier(1, v...)
+}
+
+func NewMultiEnvelopeVerifier(threshold int, p ...Verifier) (*EnvelopeVerifier, error) {
+ if threshold <= 0 || threshold > len(p) {
+ return nil, errors.New("invalid threshold")
+ }
+
+ ev := EnvelopeVerifier{
+ providers: p,
+ threshold: threshold,
+ }
+
+ return &ev, nil
+}
+
+func SHA256KeyID(pub crypto.PublicKey) (string, error) {
+ // Generate public key fingerprint
+ sshpk, err := ssh.NewPublicKey(pub)
+ if err != nil {
+ return "", err
+ }
+ fingerprint := ssh.FingerprintSHA256(sshpk)
+ return fingerprint, nil
+}
+
+func removeIndex(v []Verifier, index int) []Verifier {
+ return append(v[:index], v[index+1:]...)
+}
diff --git a/dsse/verify_test.go b/dsse/verify_test.go
new file mode 100644
index 0000000..14db088
--- /dev/null
+++ b/dsse/verify_test.go
@@ -0,0 +1,382 @@
+package dsse
+
+import (
+ "context"
+ "crypto"
+ "errors"
+ "fmt"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestEnvelopeVerifier_Verify_HandlesNil(t *testing.T) {
+ verifier, err := NewEnvelopeVerifier(&mockVerifier{})
+ assert.NoError(t, err)
+
+ acceptedKeys, err := verifier.Verify(context.TODO(), nil)
+ assert.Empty(t, acceptedKeys)
+ assert.EqualError(t, err, "cannot verify a nil envelope")
+}
+
+type mockVerifier struct {
+ returnErr error
+}
+
+func (m *mockVerifier) Verify(ctx context.Context, data, sig []byte) error {
+ if m.returnErr != nil {
+ return m.returnErr
+ }
+ return nil
+}
+
+func (m *mockVerifier) KeyID() (string, error) {
+ return "mock", errors.New("Unsupported keyid")
+}
+
+func (m *mockVerifier) Public() crypto.PublicKey {
+ return "mock-public"
+}
+
+// Test against the example in the protocol specification:
+// https://github.com/secure-systems-lab/dsse/blob/master/protocol.md
+func TestVerify(t *testing.T) {
+ var keyID = "test key 123"
+ var payloadType = "http://example.com/HelloWorld"
+
+ e := Envelope{
+ Payload: "aGVsbG8gd29ybGQ=",
+ PayloadType: payloadType,
+ Signatures: []Signature{
+ {
+ KeyID: keyID,
+ Sig: "Cc3RkvYsLhlaFVd+d6FPx4ZClhqW4ZT0rnCYAfv6/ckoGdwT7g/blWNpOBuL/tZhRiVFaglOGTU8GEjm4aEaNA==",
+ },
+ },
+ }
+
+ ev, err := NewEnvelopeVerifier(&mockVerifier{})
+ assert.Nil(t, err, "unexpected error")
+ acceptedKeys, err := ev.Verify(context.TODO(), &e)
+
+ // Now verify
+ assert.Nil(t, err, "unexpected error")
+ assert.Len(t, acceptedKeys, 1, "unexpected keys")
+ assert.Equal(t, acceptedKeys[0].KeyID, "", "unexpected keyid")
+
+ // Now try an error
+ ev, err = NewEnvelopeVerifier(&mockVerifier{returnErr: errors.New("uh oh")})
+ assert.Nil(t, err, "unexpected error")
+ _, err = ev.Verify(context.TODO(), &e)
+
+ // Now verify
+ assert.Error(t, err)
+
+}
+
+func TestVerifyOneProvider(t *testing.T) {
+ var payloadType = "http://example.com/HelloWorld"
+ var payload = "hello world"
+
+ var ns nilSignerVerifier
+ signer, err := NewEnvelopeSigner(ns)
+ assert.Nil(t, err, "unexpected error")
+
+ env, err := signer.SignPayload(context.TODO(), payloadType, []byte(payload))
+ assert.Nil(t, err, "sign failed")
+
+ verifier, err := NewEnvelopeVerifier(ns)
+ assert.Nil(t, err, "unexpected error")
+ acceptedKeys, err := verifier.Verify(context.TODO(), env)
+ assert.Nil(t, err, "unexpected error")
+ assert.Len(t, acceptedKeys, 1, "unexpected keys")
+ assert.Equal(t, acceptedKeys[0].KeyID, "nil", "unexpected keyid")
+}
+
+func TestVerifyMultipleProvider(t *testing.T) {
+ var payloadType = "http://example.com/HelloWorld"
+ var payload = "hello world"
+
+ var ns nilSignerVerifier
+ var null nullSignerVerifier
+ signer, err := NewEnvelopeSigner(ns, null)
+ assert.Nil(t, err, "unexpected error")
+
+ env, err := signer.SignPayload(context.TODO(), payloadType, []byte(payload))
+ assert.Nil(t, err, "sign failed")
+
+ verifier, err := NewEnvelopeVerifier(ns, null)
+ assert.Nil(t, err, "unexpected error")
+ acceptedKeys, err := verifier.Verify(context.TODO(), env)
+ assert.Nil(t, err, "unexpected error")
+ assert.Len(t, acceptedKeys, 2, "unexpected keys")
+}
+
+func TestVerifyMultipleProviderThreshold(t *testing.T) {
+ var payloadType = "http://example.com/HelloWorld"
+ var payload = "hello world"
+
+ var ns nilSignerVerifier
+ var null nullSignerVerifier
+ signer, err := NewEnvelopeSigner(ns, null)
+ assert.Nil(t, err)
+ env, err := signer.SignPayload(context.TODO(), payloadType, []byte(payload))
+ assert.Nil(t, err, "sign failed")
+
+ verifier, err := NewMultiEnvelopeVerifier(2, ns, null)
+ assert.Nil(t, err, "unexpected error")
+ acceptedKeys, err := verifier.Verify(context.TODO(), env)
+ assert.Nil(t, err, "unexpected error")
+ assert.Len(t, acceptedKeys, 2, "unexpected keys")
+}
+
+func TestVerifyMultipleProviderThresholdErr(t *testing.T) {
+ var ns nilSignerVerifier
+ var null nullSignerVerifier
+ _, err := NewMultiEnvelopeVerifier(3, ns, null)
+ assert.Equal(t, errThreshold, err, "wrong error")
+ _, err = NewMultiEnvelopeVerifier(0, ns, null)
+ assert.Equal(t, errThreshold, err, "wrong error")
+}
+
+func TestVerifyErr(t *testing.T) {
+ var payloadType = "http://example.com/HelloWorld"
+ var payload = "hello world"
+
+ var errsv errSignerVerifier
+ signer, err := NewEnvelopeSigner(errsv)
+ assert.Nil(t, err, "unexpected error")
+
+ env, err := signer.SignPayload(context.TODO(), payloadType, []byte(payload))
+ assert.Nil(t, err, "sign failed")
+
+ verifier, err := NewEnvelopeVerifier(errsv)
+ assert.Nil(t, err, "unexpected error")
+ _, err = verifier.Verify(context.TODO(), env)
+ assert.Equal(t, errVerify, err, "wrong error")
+}
+
+func TestBadVerifier(t *testing.T) {
+ var payloadType = "http://example.com/HelloWorld"
+ var payload = "hello world"
+
+ var badv badverifier
+ signer, err := NewEnvelopeSigner(badv)
+ assert.Nil(t, err, "unexpected error")
+
+ env, err := signer.SignPayload(context.TODO(), payloadType, []byte(payload))
+ assert.Nil(t, err, "sign failed")
+
+ verifier, err := NewEnvelopeVerifier(badv)
+ assert.Nil(t, err, "unexpected error")
+ _, err = verifier.Verify(context.TODO(), env)
+ assert.NotNil(t, err, "expected error")
+}
+
+func TestVerifyNoSig(t *testing.T) {
+ var badv badverifier
+ verifier, err := NewEnvelopeVerifier(badv)
+ assert.Nil(t, err, "unexpected error")
+
+ env := &Envelope{}
+
+ _, err = verifier.Verify(context.TODO(), env)
+ assert.Equal(t, ErrNoSignature, err, "wrong error")
+}
+
+func TestVerifyBadBase64(t *testing.T) {
+ var badv badverifier
+ verifier, err := NewEnvelopeVerifier(badv)
+ assert.Nil(t, err, "unexpected error")
+
+ expectedErr := fmt.Errorf("unable to base64 decode payload (is payload in the right format?)")
+
+ t.Run("Payload", func(t *testing.T) {
+ env := &Envelope{
+ Payload: "Not base 64",
+ Signatures: []Signature{
+ {},
+ },
+ }
+
+ _, err := verifier.Verify(context.TODO(), env)
+ assert.IsType(t, expectedErr, err, "wrong error")
+ })
+
+ t.Run("Signature", func(t *testing.T) {
+ env := &Envelope{
+ Payload: "cGF5bG9hZAo=",
+ Signatures: []Signature{
+ {
+ Sig: "not base 64",
+ },
+ },
+ }
+
+ _, err := verifier.Verify(context.TODO(), env)
+ assert.IsType(t, expectedErr, err, "wrong error")
+ })
+}
+
+func TestVerifyNoMatch(t *testing.T) {
+ var payloadType = "http://example.com/HelloWorld"
+
+ var ns nilSignerVerifier
+ var null nullSignerVerifier
+ verifier, err := NewEnvelopeVerifier(ns, null)
+ assert.Nil(t, err, "unexpected error")
+
+ env := &Envelope{
+ PayloadType: payloadType,
+ Payload: "cGF5bG9hZAo=",
+ Signatures: []Signature{
+ {
+ KeyID: "not found",
+ Sig: "cGF5bG9hZAo=",
+ },
+ },
+ }
+
+ _, err = verifier.Verify(context.TODO(), env)
+ assert.NotNil(t, err, "expected error")
+}
+
+type interceptSignerVerifier struct {
+ keyID string
+ verifyRes bool
+ verifyCalled bool
+}
+
+func (i *interceptSignerVerifier) Sign(ctx context.Context, data []byte) ([]byte, error) {
+ return data, nil
+}
+
+func (i *interceptSignerVerifier) Verify(ctx context.Context, data, sig []byte) error {
+ i.verifyCalled = true
+
+ if i.verifyRes {
+ return nil
+ }
+ return errVerify
+}
+
+func (i *interceptSignerVerifier) KeyID() (string, error) {
+ return i.keyID, nil
+}
+
+func (i *interceptSignerVerifier) Public() crypto.PublicKey {
+ return "intercept-public"
+}
+
+func TestVerifyOneFail(t *testing.T) {
+ var payloadType = "http://example.com/HelloWorld"
+ var payload = "hello world"
+
+ var s1 = &interceptSignerVerifier{
+ keyID: "i1",
+ verifyRes: true,
+ }
+ var s2 = &interceptSignerVerifier{
+ keyID: "i2",
+ verifyRes: false,
+ }
+ signer, err := NewEnvelopeSigner(s1, s2)
+ assert.Nil(t, err, "unexpected error")
+
+ env, err := signer.SignPayload(context.TODO(), payloadType, []byte(payload))
+ assert.Nil(t, err, "sign failed")
+
+ verifier, err := NewEnvelopeVerifier(s1, s2)
+ assert.Nil(t, err, "unexpected error")
+ acceptedKeys, err := verifier.Verify(context.TODO(), env)
+ assert.Nil(t, err, "expected error")
+ assert.True(t, s1.verifyCalled, "verify not called")
+ assert.True(t, s2.verifyCalled, "verify not called")
+ assert.Len(t, acceptedKeys, 1, "unexpected keys")
+ assert.Equal(t, acceptedKeys[0].KeyID, "i1", "unexpected keyid")
+}
+
+func TestVerifySameKeyID(t *testing.T) {
+ var payloadType = "http://example.com/HelloWorld"
+ var payload = "hello world"
+
+ var s1 = &interceptSignerVerifier{
+ keyID: "i1",
+ verifyRes: true,
+ }
+ var s2 = &interceptSignerVerifier{
+ keyID: "i1",
+ verifyRes: true,
+ }
+ signer, err := NewEnvelopeSigner(s1, s2)
+ assert.Nil(t, err, "unexpected error")
+
+ env, err := signer.SignPayload(context.TODO(), payloadType, []byte(payload))
+ assert.Nil(t, err, "sign failed")
+
+ verifier, err := NewEnvelopeVerifier(s1, s2)
+ assert.Nil(t, err, "unexpected error")
+ acceptedKeys, err := verifier.Verify(context.TODO(), env)
+ assert.Nil(t, err, "expected error")
+ assert.True(t, s1.verifyCalled, "verify not called")
+ assert.True(t, s2.verifyCalled, "verify not called")
+ assert.Len(t, acceptedKeys, 1, "unexpected keys")
+ assert.Equal(t, acceptedKeys[0].KeyID, "i1", "unexpected keyid")
+}
+
+func TestVerifyEmptyKeyID(t *testing.T) {
+ var payloadType = "http://example.com/HelloWorld"
+ var payload = "hello world"
+
+ var s1 = &interceptSignerVerifier{
+ keyID: "",
+ verifyRes: true,
+ }
+
+ var s2 = &interceptSignerVerifier{
+ keyID: "",
+ verifyRes: true,
+ }
+
+ signer, err := NewEnvelopeSigner(s1, s2)
+ assert.Nil(t, err, "unexpected error")
+
+ env, err := signer.SignPayload(context.TODO(), payloadType, []byte(payload))
+ assert.Nil(t, err, "sign failed")
+
+ verifier, err := NewEnvelopeVerifier(s1, s2)
+ assert.Nil(t, err, "unexpected error")
+ acceptedKeys, err := verifier.Verify(context.TODO(), env)
+ assert.Nil(t, err, "expected error")
+ assert.Len(t, acceptedKeys, 1, "unexpected keys")
+ assert.Equal(t, acceptedKeys[0].KeyID, "", "unexpected keyid")
+}
+
+func TestVerifyPublicKeyID(t *testing.T) {
+ var payloadType = "http://example.com/HelloWorld"
+ var payload = "hello world"
+ var keyID = "SHA256:f4AuBLdH4Lj/dIuwAUXXebzoI9B/cJ4iSQ3/qByIl4M"
+
+ var s1 = &ecdsaSignerVerifier{
+ keyID: "",
+ key: newEcdsaKey(),
+ }
+
+ var s2 = &ecdsaSignerVerifier{
+ keyID: "",
+ key: newEcdsaKey(),
+ }
+
+ signer, err := NewEnvelopeSigner(s1, s2)
+ assert.Nil(t, err, "unexpected error")
+
+ env, err := signer.SignPayload(context.TODO(), payloadType, []byte(payload))
+ assert.Nil(t, err, "sign failed")
+
+ verifier, err := NewEnvelopeVerifier(s1, s2)
+ assert.Nil(t, err, "unexpected error")
+ acceptedKeys, err := verifier.Verify(context.TODO(), env)
+ assert.Nil(t, err, "expected error")
+ assert.Len(t, acceptedKeys, 1, "unexpected keys")
+ assert.Equal(t, acceptedKeys[0].KeyID, keyID, "unexpected keyid")
+}
diff --git a/encrypted/encrypted.go b/encrypted/encrypted.go
new file mode 100644
index 0000000..037a718
--- /dev/null
+++ b/encrypted/encrypted.go
@@ -0,0 +1,290 @@
+// Package encrypted provides a simple, secure system for encrypting data
+// symmetrically with a passphrase.
+//
+// It uses scrypt derive a key from the passphrase and the NaCl secret box
+// cipher for authenticated encryption.
+package encrypted
+
+import (
+ "crypto/rand"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+
+ "golang.org/x/crypto/nacl/secretbox"
+ "golang.org/x/crypto/scrypt"
+)
+
+const saltSize = 32
+
+const (
+ boxKeySize = 32
+ boxNonceSize = 24
+)
+
+// KDFParameterStrength defines the KDF parameter strength level to be used for
+// encryption key derivation.
+type KDFParameterStrength uint8
+
+const (
+ // Legacy defines legacy scrypt parameters (N:2^15, r:8, p:1)
+ Legacy KDFParameterStrength = iota + 1
+ // Standard defines standard scrypt parameters which is focusing 100ms of computation (N:2^16, r:8, p:1)
+ Standard
+ // OWASP defines OWASP recommended scrypt parameters (N:2^17, r:8, p:1)
+ OWASP
+)
+
+var (
+ // legacyParams represents old scrypt derivation parameters for backward
+ // compatibility.
+ legacyParams = scryptParams{
+ N: 32768, // 2^15
+ R: 8,
+ P: 1,
+ }
+
+ // standardParams defines scrypt parameters based on the scrypt creator
+ // recommendation to limit key derivation in time boxed to 100ms.
+ standardParams = scryptParams{
+ N: 65536, // 2^16
+ R: 8,
+ P: 1,
+ }
+
+ // owaspParams defines scrypt parameters recommended by OWASP
+ owaspParams = scryptParams{
+ N: 131072, // 2^17
+ R: 8,
+ P: 1,
+ }
+
+ // defaultParams defines scrypt parameters which will be used to generate a
+ // new key.
+ defaultParams = standardParams
+)
+
+const (
+ nameScrypt = "scrypt"
+ nameSecretBox = "nacl/secretbox"
+)
+
+type data struct {
+ KDF scryptKDF `json:"kdf"`
+ Cipher secretBoxCipher `json:"cipher"`
+ Ciphertext []byte `json:"ciphertext"`
+}
+
+type scryptParams struct {
+ N int `json:"N"`
+ R int `json:"r"`
+ P int `json:"p"`
+}
+
+func (sp *scryptParams) Equal(in *scryptParams) bool {
+ return in != nil && sp.N == in.N && sp.P == in.P && sp.R == in.R
+}
+
+func newScryptKDF(level KDFParameterStrength) (scryptKDF, error) {
+ salt := make([]byte, saltSize)
+ if err := fillRandom(salt); err != nil {
+ return scryptKDF{}, fmt.Errorf("unable to generate a random salt: %w", err)
+ }
+
+ var params scryptParams
+ switch level {
+ case Legacy:
+ params = legacyParams
+ case Standard:
+ params = standardParams
+ case OWASP:
+ params = owaspParams
+ default:
+ // Fallback to default parameters
+ params = defaultParams
+ }
+
+ return scryptKDF{
+ Name: nameScrypt,
+ Params: params,
+ Salt: salt,
+ }, nil
+}
+
+type scryptKDF struct {
+ Name string `json:"name"`
+ Params scryptParams `json:"params"`
+ Salt []byte `json:"salt"`
+}
+
+func (s *scryptKDF) Key(passphrase []byte) ([]byte, error) {
+ return scrypt.Key(passphrase, s.Salt, s.Params.N, s.Params.R, s.Params.P, boxKeySize)
+}
+
+// CheckParams checks that the encoded KDF parameters are what we expect them to
+// be. If we do not do this, an attacker could cause a DoS by tampering with
+// them.
+func (s *scryptKDF) CheckParams() error {
+ switch {
+ case legacyParams.Equal(&s.Params):
+ case standardParams.Equal(&s.Params):
+ case owaspParams.Equal(&s.Params):
+ default:
+ return errors.New("unsupported scrypt parameters")
+ }
+
+ return nil
+}
+
+func newSecretBoxCipher() (secretBoxCipher, error) {
+ nonce := make([]byte, boxNonceSize)
+ if err := fillRandom(nonce); err != nil {
+ return secretBoxCipher{}, err
+ }
+ return secretBoxCipher{
+ Name: nameSecretBox,
+ Nonce: nonce,
+ }, nil
+}
+
+type secretBoxCipher struct {
+ Name string `json:"name"`
+ Nonce []byte `json:"nonce"`
+
+ encrypted bool
+}
+
+func (s *secretBoxCipher) Encrypt(plaintext, key []byte) []byte {
+ var keyBytes [boxKeySize]byte
+ var nonceBytes [boxNonceSize]byte
+
+ if len(key) != len(keyBytes) {
+ panic("incorrect key size")
+ }
+ if len(s.Nonce) != len(nonceBytes) {
+ panic("incorrect nonce size")
+ }
+
+ copy(keyBytes[:], key)
+ copy(nonceBytes[:], s.Nonce)
+
+ // ensure that we don't re-use nonces
+ if s.encrypted {
+ panic("Encrypt must only be called once for each cipher instance")
+ }
+ s.encrypted = true
+
+ return secretbox.Seal(nil, plaintext, &nonceBytes, &keyBytes)
+}
+
+func (s *secretBoxCipher) Decrypt(ciphertext, key []byte) ([]byte, error) {
+ var keyBytes [boxKeySize]byte
+ var nonceBytes [boxNonceSize]byte
+
+ if len(key) != len(keyBytes) {
+ panic("incorrect key size")
+ }
+ if len(s.Nonce) != len(nonceBytes) {
+ // return an error instead of panicking since the nonce is user input
+ return nil, errors.New("encrypted: incorrect nonce size")
+ }
+
+ copy(keyBytes[:], key)
+ copy(nonceBytes[:], s.Nonce)
+
+ res, ok := secretbox.Open(nil, ciphertext, &nonceBytes, &keyBytes)
+ if !ok {
+ return nil, errors.New("encrypted: decryption failed")
+ }
+ return res, nil
+}
+
+// Encrypt takes a passphrase and plaintext, and returns a JSON object
+// containing ciphertext and the details necessary to decrypt it.
+func Encrypt(plaintext, passphrase []byte) ([]byte, error) {
+ return EncryptWithCustomKDFParameters(plaintext, passphrase, Standard)
+}
+
+// EncryptWithCustomKDFParameters takes a passphrase, the plaintext and a KDF
+// parameter level (Legacy, Standard, or OWASP), and returns a JSON object
+// containing ciphertext and the details necessary to decrypt it.
+func EncryptWithCustomKDFParameters(plaintext, passphrase []byte, kdfLevel KDFParameterStrength) ([]byte, error) {
+ k, err := newScryptKDF(kdfLevel)
+ if err != nil {
+ return nil, err
+ }
+ key, err := k.Key(passphrase)
+ if err != nil {
+ return nil, err
+ }
+
+ c, err := newSecretBoxCipher()
+ if err != nil {
+ return nil, err
+ }
+
+ data := &data{
+ KDF: k,
+ Cipher: c,
+ }
+ data.Ciphertext = c.Encrypt(plaintext, key)
+
+ return json.Marshal(data)
+}
+
+// Marshal encrypts the JSON encoding of v using passphrase.
+func Marshal(v interface{}, passphrase []byte) ([]byte, error) {
+ return MarshalWithCustomKDFParameters(v, passphrase, Standard)
+}
+
+// MarshalWithCustomKDFParameters encrypts the JSON encoding of v using passphrase.
+func MarshalWithCustomKDFParameters(v interface{}, passphrase []byte, kdfLevel KDFParameterStrength) ([]byte, error) {
+ data, err := json.MarshalIndent(v, "", "\t")
+ if err != nil {
+ return nil, err
+ }
+ return EncryptWithCustomKDFParameters(data, passphrase, kdfLevel)
+}
+
+// Decrypt takes a JSON-encoded ciphertext object encrypted using Encrypt and
+// tries to decrypt it using passphrase. If successful, it returns the
+// plaintext.
+func Decrypt(ciphertext, passphrase []byte) ([]byte, error) {
+ data := &data{}
+ if err := json.Unmarshal(ciphertext, data); err != nil {
+ return nil, err
+ }
+
+ if data.KDF.Name != nameScrypt {
+ return nil, fmt.Errorf("encrypted: unknown kdf name %q", data.KDF.Name)
+ }
+ if data.Cipher.Name != nameSecretBox {
+ return nil, fmt.Errorf("encrypted: unknown cipher name %q", data.Cipher.Name)
+ }
+ if err := data.KDF.CheckParams(); err != nil {
+ return nil, err
+ }
+
+ key, err := data.KDF.Key(passphrase)
+ if err != nil {
+ return nil, err
+ }
+
+ return data.Cipher.Decrypt(data.Ciphertext, key)
+}
+
+// Unmarshal decrypts the data using passphrase and unmarshals the resulting
+// plaintext into the value pointed to by v.
+func Unmarshal(data []byte, v interface{}, passphrase []byte) error {
+ decrypted, err := Decrypt(data, passphrase)
+ if err != nil {
+ return err
+ }
+ return json.Unmarshal(decrypted, v)
+}
+
+func fillRandom(b []byte) error {
+ _, err := io.ReadFull(rand.Reader, b)
+ return err
+}
diff --git a/encrypted/encrypted_test.go b/encrypted/encrypted_test.go
new file mode 100644
index 0000000..7365fb3
--- /dev/null
+++ b/encrypted/encrypted_test.go
@@ -0,0 +1,150 @@
+package encrypted
+
+import (
+ "encoding/json"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+var (
+ kdfVectors = map[KDFParameterStrength][]byte{
+ Legacy: []byte(`{"kdf":{"name":"scrypt","params":{"N":32768,"r":8,"p":1},"salt":"WO3mVvyTwJ9vwT5/Tk5OW5WPIBUofMjcpEfrLnfY4uA="},"cipher":{"name":"nacl/secretbox","nonce":"tCy7HcTFr4uxv4Nrg/DWmncuZ148U1MX"},"ciphertext":"08n43p5G5yviPEZpO7tPPF4aZQkWiWjkv4taFdhDBA0tamKH4nw="}`),
+ Standard: []byte(`{"kdf":{"name":"scrypt","params":{"N":65536,"r":8,"p":1},"salt":"FhzPOt9/bJG4PTq6lQ6ecG6GzaOuOy/ynG5+yRiFlNs="},"cipher":{"name":"nacl/secretbox","nonce":"aw1ng1jHaDz/tQ7V2gR9O2+IGQ8xJEuE"},"ciphertext":"HycvuLZL4sYH0BrYTh4E/H20VtAW6u5zL5Pr+IBjYLYnCPzDkq8="}`),
+ OWASP: []byte(`{"kdf":{"name":"scrypt","params":{"N":131072,"r":8,"p":1},"salt":"m38E3kouJTtiheLQN22NQ8DTito5hrjpUIskqcd375k="},"cipher":{"name":"nacl/secretbox","nonce":"Y6PM13yA+o44pE/W1ZBwczeGnTV/m9Zc"},"ciphertext":"6H8sqj1K6B6yDjtH5AQ6lbFigg/C2yDDJc4rYJ79w9aVPImFIPI="}`),
+ }
+)
+
+var plaintext = []byte("reallyimportant")
+
+func TestRoundtrip(t *testing.T) {
+ passphrase := []byte("supersecret")
+
+ enc, err := Encrypt(plaintext, passphrase)
+ assert.Nil(t, err)
+
+ // successful decrypt
+ dec, err := Decrypt(enc, passphrase)
+ assert.Nil(t, err)
+ assert.Equal(t, plaintext, dec)
+
+ // wrong passphrase
+ passphrase[0] = 0
+ dec, err = Decrypt(enc, passphrase)
+ assert.NotNil(t, err)
+ assert.Nil(t, dec)
+}
+
+func TestTamperedRoundtrip(t *testing.T) {
+ passphrase := []byte("supersecret")
+
+ enc, err := Encrypt(plaintext, passphrase)
+ assert.Nil(t, err)
+
+ data := &data{}
+ err = json.Unmarshal(enc, data)
+ assert.Nil(t, err)
+
+ data.Ciphertext[0] = ^data.Ciphertext[0]
+
+ enc, _ = json.Marshal(data)
+
+ dec, err := Decrypt(enc, passphrase)
+ assert.NotNil(t, err)
+ assert.Nil(t, dec)
+}
+
+func TestDecrypt(t *testing.T) {
+ enc := []byte(`{"kdf":{"name":"scrypt","params":{"N":32768,"r":8,"p":1},"salt":"N9a7x5JFGbrtB2uBR81jPwp0eiLR4A7FV3mjVAQrg1g="},"cipher":{"name":"nacl/secretbox","nonce":"2h8HxMmgRfuYdpswZBQaU3xJ1nkA/5Ik"},"ciphertext":"SEW6sUh0jf2wfdjJGPNS9+bkk2uB+Cxamf32zR8XkQ=="}`)
+ passphrase := []byte("supersecret")
+
+ dec, err := Decrypt(enc, passphrase)
+ assert.Nil(t, err)
+ assert.Equal(t, plaintext, dec)
+}
+
+func TestMarshalUnmarshal(t *testing.T) {
+ passphrase := []byte("supersecret")
+
+ wrapped, err := Marshal(plaintext, passphrase)
+ assert.Nil(t, err)
+ assert.NotNil(t, wrapped)
+
+ var protected []byte
+ err = Unmarshal(wrapped, &protected, passphrase)
+ assert.Nil(t, err)
+ assert.Equal(t, plaintext, protected)
+}
+
+func TestInvalidKDFSettings(t *testing.T) {
+ passphrase := []byte("supersecret")
+
+ wrapped, err := MarshalWithCustomKDFParameters(plaintext, passphrase, 0)
+ assert.Nil(t, err)
+ assert.NotNil(t, wrapped)
+
+ var protected []byte
+ err = Unmarshal(wrapped, &protected, passphrase)
+ assert.Nil(t, err)
+ assert.Equal(t, plaintext, protected)
+}
+
+func TestLegacyKDFSettings(t *testing.T) {
+ passphrase := []byte("supersecret")
+
+ wrapped, err := MarshalWithCustomKDFParameters(plaintext, passphrase, Legacy)
+ assert.Nil(t, err)
+ assert.NotNil(t, wrapped)
+
+ var protected []byte
+ err = Unmarshal(wrapped, &protected, passphrase)
+ assert.Nil(t, err)
+ assert.Equal(t, plaintext, protected)
+}
+
+func TestStandardKDFSettings(t *testing.T) {
+ passphrase := []byte("supersecret")
+
+ wrapped, err := MarshalWithCustomKDFParameters(plaintext, passphrase, Standard)
+ assert.Nil(t, err)
+ assert.NotNil(t, wrapped)
+
+ var protected []byte
+ err = Unmarshal(wrapped, &protected, passphrase)
+ assert.Nil(t, err)
+ assert.Equal(t, plaintext, protected)
+}
+
+func TestOWASPKDFSettings(t *testing.T) {
+ passphrase := []byte("supersecret")
+
+ wrapped, err := MarshalWithCustomKDFParameters(plaintext, passphrase, OWASP)
+ assert.Nil(t, err)
+ assert.NotNil(t, wrapped)
+
+ var protected []byte
+ err = Unmarshal(wrapped, &protected, passphrase)
+ assert.Nil(t, err)
+ assert.Equal(t, plaintext, protected)
+}
+
+func TestKDFSettingVectors(t *testing.T) {
+ passphrase := []byte("supersecret")
+
+ for _, v := range kdfVectors {
+ var protected []byte
+ err := Unmarshal(v, &protected, passphrase)
+ assert.Nil(t, err)
+ assert.Equal(t, plaintext, protected)
+ }
+}
+
+func TestUnsupportedKDFParameters(t *testing.T) {
+ enc := []byte(`{"kdf":{"name":"scrypt","params":{"N":99,"r":99,"p":99},"salt":"cZFcQJdwPhPyhU1R4qkl0qVOIjZd4V/7LYYAavq166k="},"cipher":{"name":"nacl/secretbox","nonce":"7vhRS7j0hEPBWV05skAdgLj81AkGeE7U"},"ciphertext":"6WYU/YSXVbYzl/NzaeAzmjLyfFhOOjLc0d8/GFV0aBFdJvyCcXc="}`)
+ passphrase := []byte("supersecret")
+
+ dec, err := Decrypt(enc, passphrase)
+ assert.NotNil(t, err)
+ assert.Nil(t, dec)
+ assert.ErrorContains(t, err, "unsupported scrypt parameters")
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..164adff
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,16 @@
+module github.com/secure-systems-lab/go-securesystemslib
+
+go 1.20
+
+require (
+ github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb
+ github.com/stretchr/testify v1.8.4
+ golang.org/x/crypto v0.17.0
+)
+
+require (
+ github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
+ golang.org/x/sys v0.15.0 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..5f6f01f
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,17 @@
+github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE=
+github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
+golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
+golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
+golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/signerverifier/ecdsa.go b/signerverifier/ecdsa.go
new file mode 100644
index 0000000..f3e6c20
--- /dev/null
+++ b/signerverifier/ecdsa.go
@@ -0,0 +1,111 @@
+package signerverifier
+
+import (
+ "context"
+ "crypto"
+ "crypto/ecdsa"
+ "crypto/rand"
+ "crypto/sha256"
+ "crypto/sha512"
+ "fmt"
+ "os"
+)
+
+const ECDSAKeyType = "ecdsa"
+
+// ECDSASignerVerifier is a dsse.SignerVerifier compliant interface to sign and
+// verify signatures using ECDSA keys.
+type ECDSASignerVerifier struct {
+ keyID string
+ curveSize int
+ private *ecdsa.PrivateKey
+ public *ecdsa.PublicKey
+}
+
+// NewECDSASignerVerifierFromSSLibKey creates an ECDSASignerVerifier from an
+// SSLibKey.
+func NewECDSASignerVerifierFromSSLibKey(key *SSLibKey) (*ECDSASignerVerifier, error) {
+ if len(key.KeyVal.Public) == 0 {
+ return nil, ErrInvalidKey
+ }
+
+ _, publicParsedKey, err := decodeAndParsePEM([]byte(key.KeyVal.Public))
+ if err != nil {
+ return nil, fmt.Errorf("unable to create ECDSA signerverifier: %w", err)
+ }
+
+ sv := &ECDSASignerVerifier{
+ keyID: key.KeyID,
+ curveSize: publicParsedKey.(*ecdsa.PublicKey).Params().BitSize,
+ public: publicParsedKey.(*ecdsa.PublicKey),
+ private: nil,
+ }
+
+ if len(key.KeyVal.Private) > 0 {
+ _, privateParsedKey, err := decodeAndParsePEM([]byte(key.KeyVal.Private))
+ if err != nil {
+ return nil, fmt.Errorf("unable to create ECDSA signerverifier: %w", err)
+ }
+
+ sv.private = privateParsedKey.(*ecdsa.PrivateKey)
+ }
+
+ return sv, nil
+}
+
+// Sign creates a signature for `data`.
+func (sv *ECDSASignerVerifier) Sign(ctx context.Context, data []byte) ([]byte, error) {
+ if sv.private == nil {
+ return nil, ErrNotPrivateKey
+ }
+
+ hashedData := getECDSAHashedData(data, sv.curveSize)
+
+ return ecdsa.SignASN1(rand.Reader, sv.private, hashedData)
+}
+
+// Verify verifies the `sig` value passed in against `data`.
+func (sv *ECDSASignerVerifier) Verify(ctx context.Context, data []byte, sig []byte) error {
+ hashedData := getECDSAHashedData(data, sv.curveSize)
+
+ if ok := ecdsa.VerifyASN1(sv.public, hashedData, sig); !ok {
+ return ErrSignatureVerificationFailed
+ }
+
+ return nil
+}
+
+// KeyID returns the identifier of the key used to create the
+// ECDSASignerVerifier instance.
+func (sv *ECDSASignerVerifier) KeyID() (string, error) {
+ return sv.keyID, nil
+}
+
+// Public returns the public portion of the key used to create the
+// ECDSASignerVerifier instance.
+func (sv *ECDSASignerVerifier) Public() crypto.PublicKey {
+ return sv.public
+}
+
+// LoadECDSAKeyFromFile returns an SSLibKey instance for an ECDSA key stored in
+// a file in the custom securesystemslib format.
+func LoadECDSAKeyFromFile(path string) (*SSLibKey, error) {
+ contents, err := os.ReadFile(path)
+ if err != nil {
+ return nil, fmt.Errorf("unable to load ECDSA key from file: %w", err)
+ }
+
+ return LoadKeyFromSSLibBytes(contents)
+}
+
+func getECDSAHashedData(data []byte, curveSize int) []byte {
+ switch {
+ case curveSize <= 256:
+ return hashBeforeSigning(data, sha256.New())
+ case 256 < curveSize && curveSize <= 384:
+ return hashBeforeSigning(data, sha512.New384())
+ case curveSize > 384:
+ return hashBeforeSigning(data, sha512.New())
+ }
+ return []byte{}
+}
diff --git a/signerverifier/ecdsa_test.go b/signerverifier/ecdsa_test.go
new file mode 100644
index 0000000..71163dc
--- /dev/null
+++ b/signerverifier/ecdsa_test.go
@@ -0,0 +1,191 @@
+package signerverifier
+
+import (
+ "context"
+ "encoding/json"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/secure-systems-lab/go-securesystemslib/cjson"
+ "github.com/secure-systems-lab/go-securesystemslib/dsse"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestNewECDSASignerVerifierFromSSLibKey(t *testing.T) {
+ key, err := LoadECDSAKeyFromFile(filepath.Join("test-data", "ecdsa-test-key.pub"))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ sv, err := NewECDSASignerVerifierFromSSLibKey(key)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ expectedPublicString := "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEu+HEqqpXLa48lXH9rkRygsfsCKq1\nXM36oXymJ9wxpM68nCqkrZCVnZ9lkEeCwD8qWYTNxD5yfWXwJjFh+K7qLQ==\n-----END PUBLIC KEY-----"
+ _, expectedPublicKey, err := decodeAndParsePEM([]byte(expectedPublicString))
+ assert.Nil(t, err)
+
+ assert.Equal(t, "98adf38602c48c5479e9a991ee3f8cbf541ee4f985e00f7a5fc4148d9a45b704", sv.keyID)
+ assert.Equal(t, expectedPublicKey, sv.public)
+ assert.Nil(t, sv.private)
+}
+
+func TestLoadECDSAKeyFromFile(t *testing.T) {
+ t.Run("ecdsa public key", func(t *testing.T) {
+ key, err := LoadECDSAKeyFromFile(filepath.Join("test-data", "ecdsa-test-key.pub"))
+ assert.Nil(t, err)
+
+ assert.Equal(t, "98adf38602c48c5479e9a991ee3f8cbf541ee4f985e00f7a5fc4148d9a45b704", key.KeyID)
+ assert.Equal(t, "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEu+HEqqpXLa48lXH9rkRygsfsCKq1\nXM36oXymJ9wxpM68nCqkrZCVnZ9lkEeCwD8qWYTNxD5yfWXwJjFh+K7qLQ==\n-----END PUBLIC KEY-----", key.KeyVal.Public)
+ assert.Equal(t, "ecdsa-sha2-nistp256", key.Scheme)
+ assert.Equal(t, ECDSAKeyType, key.KeyType)
+ })
+
+ t.Run("ecdsa private key", func(t *testing.T) {
+ key, err := LoadECDSAKeyFromFile(filepath.Join("test-data", "ecdsa-test-key"))
+ assert.Nil(t, err)
+
+ assert.Equal(t, "98adf38602c48c5479e9a991ee3f8cbf541ee4f985e00f7a5fc4148d9a45b704", key.KeyID)
+ assert.Equal(t, "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEu+HEqqpXLa48lXH9rkRygsfsCKq1\nXM36oXymJ9wxpM68nCqkrZCVnZ9lkEeCwD8qWYTNxD5yfWXwJjFh+K7qLQ==\n-----END PUBLIC KEY-----", key.KeyVal.Public)
+ assert.Equal(t, "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIAo6DxXlgqYy+TkvocIOyWlqA3KVtp6dlSY7lS3kkeEMoAoGCCqGSM49\nAwEHoUQDQgAEu+HEqqpXLa48lXH9rkRygsfsCKq1XM36oXymJ9wxpM68nCqkrZCV\nnZ9lkEeCwD8qWYTNxD5yfWXwJjFh+K7qLQ==\n-----END EC PRIVATE KEY-----", key.KeyVal.Private)
+ assert.Equal(t, "ecdsa-sha2-nistp256", key.Scheme)
+ assert.Equal(t, ECDSAKeyType, key.KeyType)
+ })
+
+ t.Run("invalid path", func(t *testing.T) {
+ _, err := LoadECDSAKeyFromFile(filepath.Join("test-data", "invalid"))
+ assert.ErrorContains(t, err, "unable to load ECDSA key from file")
+ })
+}
+
+func TestECDSASignerVerifierSign(t *testing.T) {
+ t.Run("using valid key", func(t *testing.T) {
+ key, err := LoadECDSAKeyFromFile(filepath.Join("test-data", "ecdsa-test-key"))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ sv, err := NewECDSASignerVerifierFromSSLibKey(key)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ message := []byte("test message")
+
+ signature, err := sv.Sign(context.Background(), message)
+ assert.Nil(t, err)
+
+ err = sv.Verify(context.Background(), message, signature)
+ assert.Nil(t, err)
+ })
+
+ t.Run("using invalid key", func(t *testing.T) {
+ key, err := LoadECDSAKeyFromFile(filepath.Join("test-data", "ecdsa-test-key.pub"))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ sv, err := NewECDSASignerVerifierFromSSLibKey(key)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ message := []byte("test message")
+
+ _, err = sv.Sign(context.Background(), message)
+ assert.ErrorIs(t, err, ErrNotPrivateKey)
+ })
+}
+
+func TestECDSASignerVerifierWithDSSEEnvelope(t *testing.T) {
+ key, err := LoadECDSAKeyFromFile(filepath.Join("test-data", "ecdsa-test-key"))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ sv, err := NewECDSASignerVerifierFromSSLibKey(key)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ payloadType := "application/vnd.dsse+json"
+ payload := []byte("test message")
+
+ es, err := dsse.NewEnvelopeSigner(sv)
+ if err != nil {
+ t.Error(err)
+ }
+
+ env, err := es.SignPayload(context.Background(), payloadType, payload)
+ if err != nil {
+ t.Error(err)
+ }
+
+ assert.Equal(t, "98adf38602c48c5479e9a991ee3f8cbf541ee4f985e00f7a5fc4148d9a45b704", env.Signatures[0].KeyID)
+ envPayload, err := env.DecodeB64Payload()
+ assert.Equal(t, payload, envPayload)
+ assert.Nil(t, err)
+
+ key, err = LoadECDSAKeyFromFile(filepath.Join("test-data", "ecdsa-test-key.pub"))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ sv, err = NewECDSASignerVerifierFromSSLibKey(key)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ ev, err := dsse.NewEnvelopeVerifier(sv)
+ if err != nil {
+ t.Error(err)
+ }
+
+ acceptedKeys, err := ev.Verify(context.Background(), env)
+ assert.Nil(t, err)
+ assert.Equal(t, "98adf38602c48c5479e9a991ee3f8cbf541ee4f985e00f7a5fc4148d9a45b704", acceptedKeys[0].KeyID)
+}
+
+func TestECDSASignerVerifierWithMetablockFile(t *testing.T) {
+ key, err := LoadECDSAKeyFromFile(filepath.Join("test-data", "ecdsa-test-key.pub"))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ sv, err := NewECDSASignerVerifierFromSSLibKey(key)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ metadataBytes, err := os.ReadFile(filepath.Join("test-data", "test-ecdsa.98adf386.link"))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ mb := struct {
+ Signatures []struct {
+ KeyID string `json:"keyid"`
+ Sig string `json:"sig"`
+ } `json:"signatures"`
+ Signed any `json:"signed"`
+ }{}
+
+ if err := json.Unmarshal(metadataBytes, &mb); err != nil {
+ t.Fatal(err)
+ }
+
+ assert.Equal(t, "304502201fbb03c0937504182a48c66f9218bdcb2e99a07ada273e92e5e543867f98c8d7022100dbfa7bbf74fd76d76c1d08676419cba85bbd81dfb000f3ac6a786693ddc508f5", mb.Signatures[0].Sig)
+ assert.Equal(t, sv.keyID, mb.Signatures[0].KeyID)
+
+ encodedBytes, err := cjson.EncodeCanonical(mb.Signed)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ decodedSig := hexDecode(t, mb.Signatures[0].Sig)
+
+ err = sv.Verify(context.Background(), encodedBytes, decodedSig)
+ assert.Nil(t, err)
+}
diff --git a/signerverifier/ed25519.go b/signerverifier/ed25519.go
new file mode 100644
index 0000000..0a2210c
--- /dev/null
+++ b/signerverifier/ed25519.go
@@ -0,0 +1,98 @@
+package signerverifier
+
+import (
+ "context"
+ "crypto"
+ "crypto/ed25519"
+ "encoding/hex"
+ "fmt"
+ "os"
+)
+
+const ED25519KeyType = "ed25519"
+
+// ED25519SignerVerifier is a dsse.SignerVerifier compliant interface to sign
+// and verify signatures using ED25519 keys.
+type ED25519SignerVerifier struct {
+ keyID string
+ private ed25519.PrivateKey
+ public ed25519.PublicKey
+}
+
+// NewED25519SignerVerifierFromSSLibKey creates an Ed25519SignerVerifier from an
+// SSLibKey.
+func NewED25519SignerVerifierFromSSLibKey(key *SSLibKey) (*ED25519SignerVerifier, error) {
+ if len(key.KeyVal.Public) == 0 {
+ return nil, ErrInvalidKey
+ }
+
+ public, err := hex.DecodeString(key.KeyVal.Public)
+ if err != nil {
+ return nil, fmt.Errorf("unable to create ED25519 signerverifier: %w", err)
+ }
+
+ var private []byte
+ if len(key.KeyVal.Private) > 0 {
+ private, err = hex.DecodeString(key.KeyVal.Private)
+ if err != nil {
+ return nil, fmt.Errorf("unable to create ED25519 signerverifier: %w", err)
+ }
+
+ // python-securesystemslib provides an interface to generate ed25519
+ // keys but it differs slightly in how it serializes the key to disk.
+ // Specifically, the keyval.private field includes _only_ the private
+ // portion of the key while libraries such as crypto/ed25519 also expect
+ // the public portion. So, if the private portion is half of what we
+ // expect, we append the public portion as well.
+ if len(private) == ed25519.PrivateKeySize/2 {
+ private = append(private, public...)
+ }
+ }
+
+ return &ED25519SignerVerifier{
+ keyID: key.KeyID,
+ public: ed25519.PublicKey(public),
+ private: ed25519.PrivateKey(private),
+ }, nil
+}
+
+// Sign creates a signature for `data`.
+func (sv *ED25519SignerVerifier) Sign(ctx context.Context, data []byte) ([]byte, error) {
+ if len(sv.private) == 0 {
+ return nil, ErrNotPrivateKey
+ }
+
+ signature := ed25519.Sign(sv.private, data)
+ return signature, nil
+}
+
+// Verify verifies the `sig` value passed in against `data`.
+func (sv *ED25519SignerVerifier) Verify(ctx context.Context, data []byte, sig []byte) error {
+ if ok := ed25519.Verify(sv.public, data, sig); ok {
+ return nil
+ }
+ return ErrSignatureVerificationFailed
+}
+
+// KeyID returns the identifier of the key used to create the
+// ED25519SignerVerifier instance.
+func (sv *ED25519SignerVerifier) KeyID() (string, error) {
+ return sv.keyID, nil
+}
+
+// Public returns the public portion of the key used to create the
+// ED25519SignerVerifier instance.
+func (sv *ED25519SignerVerifier) Public() crypto.PublicKey {
+ return sv.public
+}
+
+// LoadED25519KeyFromFile returns an SSLibKey instance for an ED25519 key stored
+// in a file in the custom securesystemslib format.
+func LoadED25519KeyFromFile(path string) (*SSLibKey, error) {
+ contents, err := os.ReadFile(path)
+ if err != nil {
+ return nil, fmt.Errorf("unable to load ED25519 key from file: %w", err)
+ }
+
+ return LoadKeyFromSSLibBytes(contents)
+}
diff --git a/signerverifier/ed25519_test.go b/signerverifier/ed25519_test.go
new file mode 100644
index 0000000..19cf91e
--- /dev/null
+++ b/signerverifier/ed25519_test.go
@@ -0,0 +1,207 @@
+package signerverifier
+
+import (
+ "context"
+ "crypto/ed25519"
+ "encoding/json"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/secure-systems-lab/go-securesystemslib/cjson"
+ "github.com/secure-systems-lab/go-securesystemslib/dsse"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestNewED25519SignerVerifierFromSSLibKey(t *testing.T) {
+ key, err := LoadED25519KeyFromFile(filepath.Join("test-data", "ed25519-test-key.pub"))
+ if err != nil {
+ t.Error(err)
+ }
+
+ sv, err := NewED25519SignerVerifierFromSSLibKey(key)
+ if err != nil {
+ t.Error(err)
+ }
+
+ expectedPublicString := "3f586ce67329419fb0081bd995914e866a7205da463d593b3b490eab2b27fd3f"
+ expectedPublicKey := ed25519.PublicKey(hexDecode(t, expectedPublicString))
+
+ assert.Equal(t, "52e3b8e73279d6ebdd62a5016e2725ff284f569665eb92ccb145d83817a02997", sv.keyID)
+ assert.Equal(t, expectedPublicKey, sv.public)
+ assert.Nil(t, sv.private)
+}
+
+func TestLoadED25519KeyFromFile(t *testing.T) {
+ t.Run("ED25519 public key", func(t *testing.T) {
+ key, err := LoadED25519KeyFromFile(filepath.Join("test-data", "ed25519-test-key.pub"))
+ assert.Nil(t, err)
+
+ assert.Equal(t, "52e3b8e73279d6ebdd62a5016e2725ff284f569665eb92ccb145d83817a02997", key.KeyID)
+ assert.Equal(t, "3f586ce67329419fb0081bd995914e866a7205da463d593b3b490eab2b27fd3f", key.KeyVal.Public)
+ assert.Equal(t, "ed25519", key.Scheme)
+ assert.Equal(t, ED25519KeyType, key.KeyType)
+ })
+
+ t.Run("ED25519 private key", func(t *testing.T) {
+ key, err := LoadED25519KeyFromFile(filepath.Join("test-data", "ed25519-test-key"))
+ assert.Nil(t, err)
+
+ assert.Equal(t, "52e3b8e73279d6ebdd62a5016e2725ff284f569665eb92ccb145d83817a02997", key.KeyID)
+ assert.Equal(t, "3f586ce67329419fb0081bd995914e866a7205da463d593b3b490eab2b27fd3f", key.KeyVal.Public)
+ assert.Equal(t, "66f6ebad4aeb949b91c84c9cfd6ee351fc4fd544744bab6e30fb400ba13c6e9a", key.KeyVal.Private)
+ assert.Equal(t, "ed25519", key.Scheme)
+ assert.Equal(t, ED25519KeyType, key.KeyType)
+ })
+
+ t.Run("invalid path", func(t *testing.T) {
+ _, err := LoadED25519KeyFromFile(filepath.Join("test-data", "invalid"))
+ assert.ErrorContains(t, err, "unable to load ED25519 key from file")
+ })
+}
+
+func TestED25519SignerVerifierSign(t *testing.T) {
+ key, err := LoadED25519KeyFromFile(filepath.Join("test-data", "ed25519-test-key"))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ sv, err := NewED25519SignerVerifierFromSSLibKey(key)
+ if err != nil {
+ t.Error(err)
+ }
+
+ message := []byte("test message")
+
+ signature, err := sv.Sign(context.Background(), message)
+ if err != nil {
+ t.Error(err)
+ }
+
+ expectedSignature := []byte{0x80, 0x72, 0xb4, 0x31, 0xc5, 0xa3, 0x7e, 0xc, 0xf3, 0x91, 0x22, 0x3, 0x60, 0xbf, 0x92, 0xa4, 0x46, 0x31, 0x84, 0x83, 0xf1, 0x31, 0x3, 0xdc, 0xbc, 0x5, 0x6f, 0xab, 0x84, 0xe4, 0xdc, 0xe9, 0xf5, 0x1c, 0xa9, 0xb3, 0x95, 0xa5, 0xa0, 0x16, 0xd3, 0xaa, 0x4d, 0xe7, 0xde, 0xaf, 0xc2, 0x5e, 0x1e, 0x9a, 0x9d, 0xc8, 0xb2, 0x5c, 0x1c, 0x68, 0xf7, 0x28, 0xb4, 0x1, 0x4d, 0x9f, 0xc8, 0x4}
+ assert.Equal(t, expectedSignature, signature)
+
+ key, err = LoadED25519KeyFromFile(filepath.Join("test-data", "ed25519-test-key.pub"))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ sv, err = NewED25519SignerVerifierFromSSLibKey(key)
+ if err != nil {
+ t.Error(err)
+ }
+
+ _, err = sv.Sign(context.Background(), message)
+ assert.ErrorIs(t, err, ErrNotPrivateKey)
+}
+
+func TestED25519SignerVerifierVerify(t *testing.T) {
+ key, err := LoadED25519KeyFromFile(filepath.Join("test-data", "ed25519-test-key.pub"))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ sv, err := NewED25519SignerVerifierFromSSLibKey(key)
+ if err != nil {
+ t.Error(err)
+ }
+
+ message := []byte("test message")
+ signature := []byte{0x80, 0x72, 0xb4, 0x31, 0xc5, 0xa3, 0x7e, 0xc, 0xf3, 0x91, 0x22, 0x3, 0x60, 0xbf, 0x92, 0xa4, 0x46, 0x31, 0x84, 0x83, 0xf1, 0x31, 0x3, 0xdc, 0xbc, 0x5, 0x6f, 0xab, 0x84, 0xe4, 0xdc, 0xe9, 0xf5, 0x1c, 0xa9, 0xb3, 0x95, 0xa5, 0xa0, 0x16, 0xd3, 0xaa, 0x4d, 0xe7, 0xde, 0xaf, 0xc2, 0x5e, 0x1e, 0x9a, 0x9d, 0xc8, 0xb2, 0x5c, 0x1c, 0x68, 0xf7, 0x28, 0xb4, 0x1, 0x4d, 0x9f, 0xc8, 0x4}
+ assert.Nil(t, sv.Verify(context.Background(), message, signature))
+
+ message = []byte("corrupted message")
+ err = sv.Verify(context.Background(), message, signature)
+ assert.ErrorIs(t, err, ErrSignatureVerificationFailed)
+}
+
+func TestED25519SignerVerifierWithDSSEEnvelope(t *testing.T) {
+ key, err := LoadED25519KeyFromFile(filepath.Join("test-data", "ed25519-test-key"))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ sv, err := NewED25519SignerVerifierFromSSLibKey(key)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ payloadType := "application/vnd.dsse+json"
+ payload := []byte("test message")
+
+ es, err := dsse.NewEnvelopeSigner(sv)
+ if err != nil {
+ t.Error(err)
+ }
+
+ env, err := es.SignPayload(context.Background(), payloadType, payload)
+ if err != nil {
+ t.Error(err)
+ }
+
+ assert.Equal(t, "52e3b8e73279d6ebdd62a5016e2725ff284f569665eb92ccb145d83817a02997", env.Signatures[0].KeyID)
+ envPayload, err := env.DecodeB64Payload()
+ assert.Equal(t, payload, envPayload)
+ assert.Nil(t, err)
+
+ key, err = LoadED25519KeyFromFile(filepath.Join("test-data", "ed25519-test-key.pub"))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ sv, err = NewED25519SignerVerifierFromSSLibKey(key)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ ev, err := dsse.NewEnvelopeVerifier(sv)
+ if err != nil {
+ t.Error(err)
+ }
+
+ acceptedKeys, err := ev.Verify(context.Background(), env)
+ assert.Nil(t, err)
+ assert.Equal(t, "52e3b8e73279d6ebdd62a5016e2725ff284f569665eb92ccb145d83817a02997", acceptedKeys[0].KeyID)
+}
+
+func TestED25519SignerVerifierWithMetablockFile(t *testing.T) {
+ key, err := LoadED25519KeyFromFile(filepath.Join("test-data", "ed25519-test-key.pub"))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ sv, err := NewED25519SignerVerifierFromSSLibKey(key)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ metadataBytes, err := os.ReadFile(filepath.Join("test-data", "test-ed25519.52e3b8e7.link"))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ mb := struct {
+ Signatures []struct {
+ KeyID string `json:"keyid"`
+ Sig string `json:"sig"`
+ } `json:"signatures"`
+ Signed any `json:"signed"`
+ }{}
+
+ if err := json.Unmarshal(metadataBytes, &mb); err != nil {
+ t.Fatal(err)
+ }
+
+ assert.Equal(t, "4c8b7605a9195d4ddba54493bbb5257a9836c1d16056a027fd77e97b95a4f3e36f8bc3c9c9960387d68187760b3072a30c44f992c5bf8f7497c303a3b0a32403", mb.Signatures[0].Sig)
+ assert.Equal(t, sv.keyID, mb.Signatures[0].KeyID)
+
+ encodedBytes, err := cjson.EncodeCanonical(mb.Signed)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ decodedSig := hexDecode(t, mb.Signatures[0].Sig)
+
+ err = sv.Verify(context.Background(), encodedBytes, decodedSig)
+ assert.Nil(t, err)
+}
diff --git a/signerverifier/rsa.go b/signerverifier/rsa.go
new file mode 100644
index 0000000..b039659
--- /dev/null
+++ b/signerverifier/rsa.go
@@ -0,0 +1,161 @@
+package signerverifier
+
+import (
+ "context"
+ "crypto"
+ "crypto/rand"
+ "crypto/rsa"
+ "crypto/sha256"
+ "crypto/x509"
+ "fmt"
+ "os"
+ "strings"
+)
+
+const (
+ RSAKeyType = "rsa"
+ RSAKeyScheme = "rsassa-pss-sha256"
+ RSAPrivateKeyPEM = "RSA PRIVATE KEY"
+)
+
+// RSAPSSSignerVerifier is a dsse.SignerVerifier compliant interface to sign and
+// verify signatures using RSA keys following the RSA-PSS scheme.
+type RSAPSSSignerVerifier struct {
+ keyID string
+ private *rsa.PrivateKey
+ public *rsa.PublicKey
+}
+
+// NewRSAPSSSignerVerifierFromSSLibKey creates an RSAPSSSignerVerifier from an
+// SSLibKey.
+func NewRSAPSSSignerVerifierFromSSLibKey(key *SSLibKey) (*RSAPSSSignerVerifier, error) {
+ if len(key.KeyVal.Public) == 0 {
+ return nil, ErrInvalidKey
+ }
+
+ _, publicParsedKey, err := decodeAndParsePEM([]byte(key.KeyVal.Public))
+ if err != nil {
+ return nil, fmt.Errorf("unable to create RSA-PSS signerverifier: %w", err)
+ }
+
+ if len(key.KeyVal.Private) > 0 {
+ _, privateParsedKey, err := decodeAndParsePEM([]byte(key.KeyVal.Private))
+ if err != nil {
+ return nil, fmt.Errorf("unable to create RSA-PSS signerverifier: %w", err)
+ }
+
+ return &RSAPSSSignerVerifier{
+ keyID: key.KeyID,
+ public: publicParsedKey.(*rsa.PublicKey),
+ private: privateParsedKey.(*rsa.PrivateKey),
+ }, nil
+ }
+
+ return &RSAPSSSignerVerifier{
+ keyID: key.KeyID,
+ public: publicParsedKey.(*rsa.PublicKey),
+ private: nil,
+ }, nil
+}
+
+// Sign creates a signature for `data`.
+func (sv *RSAPSSSignerVerifier) Sign(ctx context.Context, data []byte) ([]byte, error) {
+ if sv.private == nil {
+ return nil, ErrNotPrivateKey
+ }
+
+ hashedData := hashBeforeSigning(data, sha256.New())
+
+ return rsa.SignPSS(rand.Reader, sv.private, crypto.SHA256, hashedData, &rsa.PSSOptions{SaltLength: sha256.Size, Hash: crypto.SHA256})
+}
+
+// Verify verifies the `sig` value passed in against `data`.
+func (sv *RSAPSSSignerVerifier) Verify(ctx context.Context, data []byte, sig []byte) error {
+ hashedData := hashBeforeSigning(data, sha256.New())
+
+ if err := rsa.VerifyPSS(sv.public, crypto.SHA256, hashedData, sig, &rsa.PSSOptions{SaltLength: sha256.Size, Hash: crypto.SHA256}); err != nil {
+ return ErrSignatureVerificationFailed
+ }
+
+ return nil
+}
+
+// KeyID returns the identifier of the key used to create the
+// RSAPSSSignerVerifier instance.
+func (sv *RSAPSSSignerVerifier) KeyID() (string, error) {
+ return sv.keyID, nil
+}
+
+// Public returns the public portion of the key used to create the
+// RSAPSSSignerVerifier instance.
+func (sv *RSAPSSSignerVerifier) Public() crypto.PublicKey {
+ return sv.public
+}
+
+// LoadRSAPSSKeyFromFile returns an SSLibKey instance for an RSA key stored in a
+// file.
+func LoadRSAPSSKeyFromFile(path string) (*SSLibKey, error) {
+ contents, err := os.ReadFile(path)
+ if err != nil {
+ return nil, fmt.Errorf("unable to load RSA key from file: %w", err)
+ }
+
+ return LoadRSAPSSKeyFromBytes(contents)
+}
+
+// LoadRSAPSSKeyFromBytes is a function that takes a byte array as input. This byte array should represent a PEM encoded RSA key, as PEM encoding is required.
+// The function returns an SSLibKey instance, which is a struct that holds the key data.
+
+func LoadRSAPSSKeyFromBytes(contents []byte) (*SSLibKey, error) {
+ pemData, keyObj, err := decodeAndParsePEM(contents)
+ if err != nil {
+ return nil, fmt.Errorf("unable to load RSA key from file: %w", err)
+ }
+
+ key := &SSLibKey{
+ KeyType: RSAKeyType,
+ Scheme: RSAKeyScheme,
+ KeyIDHashAlgorithms: KeyIDHashAlgorithms,
+ KeyVal: KeyVal{},
+ }
+
+ pubKeyBytes, err := marshalAndGeneratePEM(keyObj)
+ if err != nil {
+ return nil, fmt.Errorf("unable to load RSA key from file: %w", err)
+ }
+ key.KeyVal.Public = strings.TrimSpace(string(pubKeyBytes))
+
+ if _, ok := keyObj.(*rsa.PrivateKey); ok {
+ key.KeyVal.Private = strings.TrimSpace(string(generatePEMBlock(pemData.Bytes, RSAPrivateKeyPEM)))
+ }
+
+ if len(key.KeyID) == 0 {
+ keyID, err := calculateKeyID(key)
+ if err != nil {
+ return nil, fmt.Errorf("unable to load RSA key from file: %w", err)
+ }
+ key.KeyID = keyID
+ }
+
+ return key, nil
+}
+
+func marshalAndGeneratePEM(key interface{}) ([]byte, error) {
+ var pubKeyBytes []byte
+ var err error
+
+ switch k := key.(type) {
+ case *rsa.PublicKey:
+ pubKeyBytes, err = x509.MarshalPKIXPublicKey(k)
+ case *rsa.PrivateKey:
+ pubKeyBytes, err = x509.MarshalPKIXPublicKey(k.Public())
+ default:
+ return nil, fmt.Errorf("unexpected key type: %T", k)
+ }
+
+ if err != nil {
+ return nil, err
+ }
+
+ return generatePEMBlock(pubKeyBytes, PublicKeyPEM), nil
+}
diff --git a/signerverifier/rsa_test.go b/signerverifier/rsa_test.go
new file mode 100644
index 0000000..b8711d8
--- /dev/null
+++ b/signerverifier/rsa_test.go
@@ -0,0 +1,193 @@
+package signerverifier
+
+import (
+ "context"
+ "crypto/rsa"
+ "encoding/json"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/secure-systems-lab/go-securesystemslib/cjson"
+ "github.com/secure-systems-lab/go-securesystemslib/dsse"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestNewRSAPSSSignerVerifierFromSSLibKey(t *testing.T) {
+ key, err := LoadRSAPSSKeyFromFile(filepath.Join("test-data", "rsa-test-key.pub"))
+ if err != nil {
+ t.Error(err)
+ }
+
+ sv, err := NewRSAPSSSignerVerifierFromSSLibKey(key)
+ if err != nil {
+ t.Error(err)
+ }
+
+ expectedPublicString := "-----BEGIN PUBLIC KEY-----\nMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEA04egZRic+dZMVtiQc56D\nejU4FF1q3aOkUKnD+Q4lTbj1zp6ODKJTcktupmrad68jqtMiSGG8he6ELFs377q8\nbbgEUMWgAf+06Q8oFvUSfOXzZNFI7H5SMPOJY5aDWIMIEZ8DlcO7TfkA7D3iAEJX\nxxTOVS3UAIk5umO7Y7t7yXr8O/C4u78krGazCnoblcekMLJZV4O/5BloWNAe/B1c\nvZdaZUf3brD4ZZrxEtXw/tefhn1aHsSUajVW2wwjSpKhqj7Z0XS3bDS3T95/3xsN\n6+hlS6A7rJfiWpKIRHj0vh2SXLDmmhQl1In8TD/aiycTUyWcBRHVPlYFgYPt6SaT\nVQSgMzSxC43/2fINb2fyt8SbUHJ3Ct+mzRzd/1AQikWhBdstJLxInewzjYE/sb+c\n2CmCxMPQG2BwmAWXaaumeJcXVPBlMgAcjMatM8bPByTbXpKDnQslOE7g/gswDIwn\nEm53T13mZzYUvbLJ0q3aljZVLIC3IZn3ZwA2yCWchBkVAgMBAAE=\n-----END PUBLIC KEY-----"
+ _, expectedPublicKey, err := decodeAndParsePEM([]byte(expectedPublicString))
+ assert.Nil(t, err)
+
+ assert.Equal(t, "4e8d20af09fcaed6c388a186427f94a5f7ff5591ec295f4aab2cff49ffe39e9b", sv.keyID)
+ assert.Equal(t, expectedPublicKey.(*rsa.PublicKey), sv.public)
+ assert.Nil(t, sv.private)
+}
+
+func TestLoadRSAPSSKeyFromFile(t *testing.T) {
+ t.Run("RSA public key", func(t *testing.T) {
+ key, err := LoadRSAPSSKeyFromFile(filepath.Join("test-data", "rsa-test-key.pub"))
+ assert.Nil(t, err)
+
+ assert.Equal(t, "4e8d20af09fcaed6c388a186427f94a5f7ff5591ec295f4aab2cff49ffe39e9b", key.KeyID)
+ assert.Equal(t, "-----BEGIN PUBLIC KEY-----\nMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEA04egZRic+dZMVtiQc56D\nejU4FF1q3aOkUKnD+Q4lTbj1zp6ODKJTcktupmrad68jqtMiSGG8he6ELFs377q8\nbbgEUMWgAf+06Q8oFvUSfOXzZNFI7H5SMPOJY5aDWIMIEZ8DlcO7TfkA7D3iAEJX\nxxTOVS3UAIk5umO7Y7t7yXr8O/C4u78krGazCnoblcekMLJZV4O/5BloWNAe/B1c\nvZdaZUf3brD4ZZrxEtXw/tefhn1aHsSUajVW2wwjSpKhqj7Z0XS3bDS3T95/3xsN\n6+hlS6A7rJfiWpKIRHj0vh2SXLDmmhQl1In8TD/aiycTUyWcBRHVPlYFgYPt6SaT\nVQSgMzSxC43/2fINb2fyt8SbUHJ3Ct+mzRzd/1AQikWhBdstJLxInewzjYE/sb+c\n2CmCxMPQG2BwmAWXaaumeJcXVPBlMgAcjMatM8bPByTbXpKDnQslOE7g/gswDIwn\nEm53T13mZzYUvbLJ0q3aljZVLIC3IZn3ZwA2yCWchBkVAgMBAAE=\n-----END PUBLIC KEY-----", key.KeyVal.Public)
+ assert.Equal(t, RSAKeyScheme, key.Scheme)
+ assert.Equal(t, RSAKeyType, key.KeyType)
+ })
+
+ t.Run("RSA private key", func(t *testing.T) {
+ key, err := LoadRSAPSSKeyFromFile(filepath.Join("test-data", "rsa-test-key"))
+ assert.Nil(t, err)
+
+ assert.Equal(t, "4e8d20af09fcaed6c388a186427f94a5f7ff5591ec295f4aab2cff49ffe39e9b", key.KeyID)
+ assert.Equal(t, "-----BEGIN PUBLIC KEY-----\nMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEA04egZRic+dZMVtiQc56D\nejU4FF1q3aOkUKnD+Q4lTbj1zp6ODKJTcktupmrad68jqtMiSGG8he6ELFs377q8\nbbgEUMWgAf+06Q8oFvUSfOXzZNFI7H5SMPOJY5aDWIMIEZ8DlcO7TfkA7D3iAEJX\nxxTOVS3UAIk5umO7Y7t7yXr8O/C4u78krGazCnoblcekMLJZV4O/5BloWNAe/B1c\nvZdaZUf3brD4ZZrxEtXw/tefhn1aHsSUajVW2wwjSpKhqj7Z0XS3bDS3T95/3xsN\n6+hlS6A7rJfiWpKIRHj0vh2SXLDmmhQl1In8TD/aiycTUyWcBRHVPlYFgYPt6SaT\nVQSgMzSxC43/2fINb2fyt8SbUHJ3Ct+mzRzd/1AQikWhBdstJLxInewzjYE/sb+c\n2CmCxMPQG2BwmAWXaaumeJcXVPBlMgAcjMatM8bPByTbXpKDnQslOE7g/gswDIwn\nEm53T13mZzYUvbLJ0q3aljZVLIC3IZn3ZwA2yCWchBkVAgMBAAE=\n-----END PUBLIC KEY-----", key.KeyVal.Public)
+ expectedPrivateKey := "-----BEGIN RSA PRIVATE KEY-----\n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\n-----END RSA PRIVATE KEY-----"
+ assert.Equal(t, expectedPrivateKey, key.KeyVal.Private)
+ assert.Equal(t, RSAKeyScheme, key.Scheme)
+ assert.Equal(t, RSAKeyType, key.KeyType)
+ })
+
+ t.Run("invalid path", func(t *testing.T) {
+ _, err := LoadRSAPSSKeyFromFile(filepath.Join("test-data", "invalid"))
+ assert.ErrorContains(t, err, "unable to load RSA key from file")
+ })
+}
+
+func TestRSAPSSSignerVerifierSignAndVerify(t *testing.T) {
+ t.Run("using valid key", func(t *testing.T) {
+ key, err := LoadRSAPSSKeyFromFile(filepath.Join("test-data", "rsa-test-key"))
+ if err != nil {
+ t.Error(err)
+ }
+
+ sv, err := NewRSAPSSSignerVerifierFromSSLibKey(key)
+ if err != nil {
+ t.Error(err)
+ }
+
+ message := []byte("test message")
+
+ signature, err := sv.Sign(context.Background(), message)
+ assert.Nil(t, err)
+
+ err = sv.Verify(context.Background(), message, signature)
+ assert.Nil(t, err)
+ })
+
+ t.Run("using invalid key", func(t *testing.T) {
+ key, err := LoadRSAPSSKeyFromFile(filepath.Join("test-data", "rsa-test-key.pub"))
+ if err != nil {
+ t.Error(err)
+ }
+
+ sv, err := NewRSAPSSSignerVerifierFromSSLibKey(key)
+ if err != nil {
+ t.Error(err)
+ }
+
+ message := []byte("test message")
+
+ _, err = sv.Sign(context.Background(), message)
+ assert.ErrorIs(t, err, ErrNotPrivateKey)
+ })
+}
+
+func TestRSAPSSSignerVerifierWithDSSEEnvelope(t *testing.T) {
+ key, err := LoadRSAPSSKeyFromFile(filepath.Join("test-data", "rsa-test-key"))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ sv, err := NewRSAPSSSignerVerifierFromSSLibKey(key)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ payloadType := "application/vnd.dsse+json"
+ payload := []byte("test message")
+
+ es, err := dsse.NewEnvelopeSigner(sv)
+ if err != nil {
+ t.Error(err)
+ }
+
+ env, err := es.SignPayload(context.Background(), payloadType, payload)
+ if err != nil {
+ t.Error(err)
+ }
+
+ assert.Equal(t, "4e8d20af09fcaed6c388a186427f94a5f7ff5591ec295f4aab2cff49ffe39e9b", env.Signatures[0].KeyID)
+ envPayload, err := env.DecodeB64Payload()
+ assert.Equal(t, payload, envPayload)
+ assert.Nil(t, err)
+
+ key, err = LoadRSAPSSKeyFromFile(filepath.Join("test-data", "rsa-test-key.pub"))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ sv, err = NewRSAPSSSignerVerifierFromSSLibKey(key)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ ev, err := dsse.NewEnvelopeVerifier(sv)
+ if err != nil {
+ t.Error(err)
+ }
+
+ acceptedKeys, err := ev.Verify(context.Background(), env)
+ assert.Nil(t, err)
+ assert.Equal(t, "4e8d20af09fcaed6c388a186427f94a5f7ff5591ec295f4aab2cff49ffe39e9b", acceptedKeys[0].KeyID)
+}
+
+func TestRSAPSSSignerVerifierWithMetablockFile(t *testing.T) {
+ key, err := LoadRSAPSSKeyFromFile(filepath.Join("test-data", "rsa-test-key.pub"))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ sv, err := NewRSAPSSSignerVerifierFromSSLibKey(key)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ metadataBytes, err := os.ReadFile(filepath.Join("test-data", "test-rsa.4e8d20af.link"))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ mb := struct {
+ Signatures []struct {
+ KeyID string `json:"keyid"`
+ Sig string `json:"sig"`
+ } `json:"signatures"`
+ Signed any `json:"signed"`
+ }{}
+
+ if err := json.Unmarshal(metadataBytes, &mb); err != nil {
+ t.Fatal(err)
+ }
+
+ assert.Equal(t, "8958e5be66ee4352880a531bd097d1727adcc78e66b4faeb4a2cd6ad073dcb84f9a34e8156af39a7144cb5cd925325a18ccd4f0b2f981d6ff82655a7d63210d36655c50a0bf24e4839c10430a040dd6189d04fabec90eae4314c75ae2d585da17a56aaf6755e613a3a6a471ad2eddbb24504848e34f9ac163660f8ab80d7701bfa1189578a59597b3809ee62a70a7cc9545cfa65e23018fa442a45279b9fcf9d80bc92df711bfcfe16e3eae1bcf61b3286c1f0bdda17bc28bfab5b736bdcac4a38e31db1d0e0f56a2853b1b451650305f040a3425c3be47125700e92ef82c5a91a040b5e70ab7f6ebbe037ae1a6835044b5699748037e2e39a55a420c41cd9fa6e16868776367e3620e7d28eb9d8a3d710bdc98d488df1a9947d2ec8400f3c6209e8ca587cbffa30ceb3be98105e03182aab1bbb3c4e2560d99f0b09c012df2271f273ac70a6abb185abe11d559b118dca616417fa9205e74ab58e89ffd8b965da304ae9dc9cf6ffac4838b7c5375d6c2057a61cb286f06ad3b02a49c3af6178", mb.Signatures[0].Sig)
+ assert.Equal(t, sv.keyID, mb.Signatures[0].KeyID)
+
+ encodedBytes, err := cjson.EncodeCanonical(mb.Signed)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ decodedSig := hexDecode(t, mb.Signatures[0].Sig)
+
+ err = sv.Verify(context.Background(), encodedBytes, decodedSig)
+ assert.Nil(t, err)
+}
diff --git a/signerverifier/signerverifier.go b/signerverifier/signerverifier.go
new file mode 100644
index 0000000..85cae65
--- /dev/null
+++ b/signerverifier/signerverifier.go
@@ -0,0 +1,36 @@
+package signerverifier
+
+import (
+ "errors"
+)
+
+var KeyIDHashAlgorithms = []string{"sha256", "sha512"}
+
+var (
+ ErrNotPrivateKey = errors.New("loaded key is not a private key")
+ ErrSignatureVerificationFailed = errors.New("failed to verify signature")
+ ErrUnknownKeyType = errors.New("unknown key type")
+ ErrInvalidThreshold = errors.New("threshold is either less than 1 or greater than number of provided public keys")
+ ErrInvalidKey = errors.New("key object has no value")
+)
+
+const (
+ PublicKeyPEM = "PUBLIC KEY"
+ PrivateKeyPEM = "PRIVATE KEY"
+)
+
+type SSLibKey struct {
+ KeyIDHashAlgorithms []string `json:"keyid_hash_algorithms"`
+ KeyType string `json:"keytype"`
+ KeyVal KeyVal `json:"keyval"`
+ Scheme string `json:"scheme"`
+ KeyID string `json:"keyid"`
+}
+
+type KeyVal struct {
+ Private string `json:"private,omitempty"`
+ Public string `json:"public,omitempty"`
+ Certificate string `json:"certificate,omitempty"`
+ Identity string `json:"identity,omitempty"`
+ Issuer string `json:"issuer,omitempty"`
+}
diff --git a/signerverifier/test-data/ecdsa-test-key b/signerverifier/test-data/ecdsa-test-key
new file mode 100644
index 0000000..8737bc8
--- /dev/null
+++ b/signerverifier/test-data/ecdsa-test-key
@@ -0,0 +1 @@
+{"keytype": "ecdsa", "scheme": "ecdsa-sha2-nistp256", "keyid": "98adf38602c48c5479e9a991ee3f8cbf541ee4f985e00f7a5fc4148d9a45b704", "keyval": {"public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEu+HEqqpXLa48lXH9rkRygsfsCKq1\nXM36oXymJ9wxpM68nCqkrZCVnZ9lkEeCwD8qWYTNxD5yfWXwJjFh+K7qLQ==\n-----END PUBLIC KEY-----", "private": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIAo6DxXlgqYy+TkvocIOyWlqA3KVtp6dlSY7lS3kkeEMoAoGCCqGSM49\nAwEHoUQDQgAEu+HEqqpXLa48lXH9rkRygsfsCKq1XM36oXymJ9wxpM68nCqkrZCV\nnZ9lkEeCwD8qWYTNxD5yfWXwJjFh+K7qLQ==\n-----END EC PRIVATE KEY-----"}, "keyid_hash_algorithms": ["sha256", "sha512"]}
diff --git a/signerverifier/test-data/ecdsa-test-key.pub b/signerverifier/test-data/ecdsa-test-key.pub
new file mode 100755
index 0000000..cc695db
--- /dev/null
+++ b/signerverifier/test-data/ecdsa-test-key.pub
@@ -0,0 +1 @@
+{"keytype": "ecdsa", "scheme": "ecdsa-sha2-nistp256", "keyid_hash_algorithms": ["sha256", "sha512"], "keyval": {"public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEu+HEqqpXLa48lXH9rkRygsfsCKq1\nXM36oXymJ9wxpM68nCqkrZCVnZ9lkEeCwD8qWYTNxD5yfWXwJjFh+K7qLQ==\n-----END PUBLIC KEY-----"}}
diff --git a/signerverifier/test-data/ed25519-test-key b/signerverifier/test-data/ed25519-test-key
new file mode 100644
index 0000000..da14a8e
--- /dev/null
+++ b/signerverifier/test-data/ed25519-test-key
@@ -0,0 +1 @@
+{"keytype": "ed25519", "scheme": "ed25519", "keyid": "52e3b8e73279d6ebdd62a5016e2725ff284f569665eb92ccb145d83817a02997", "keyid_hash_algorithms": ["sha256", "sha512"], "keyval": {"public": "3f586ce67329419fb0081bd995914e866a7205da463d593b3b490eab2b27fd3f", "private": "66f6ebad4aeb949b91c84c9cfd6ee351fc4fd544744bab6e30fb400ba13c6e9a"}}
diff --git a/signerverifier/test-data/ed25519-test-key.pub b/signerverifier/test-data/ed25519-test-key.pub
new file mode 100644
index 0000000..7ec2a0c
--- /dev/null
+++ b/signerverifier/test-data/ed25519-test-key.pub
@@ -0,0 +1 @@
+{"keytype": "ed25519", "scheme": "ed25519", "keyid_hash_algorithms": ["sha256", "sha512"], "keyval": {"public": "3f586ce67329419fb0081bd995914e866a7205da463d593b3b490eab2b27fd3f"}}
diff --git a/signerverifier/test-data/rsa-test-key b/signerverifier/test-data/rsa-test-key
new file mode 100644
index 0000000..5571e86
--- /dev/null
+++ b/signerverifier/test-data/rsa-test-key
@@ -0,0 +1,39 @@
+-----BEGIN RSA PRIVATE KEY-----
+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
+-----END RSA PRIVATE KEY-----
diff --git a/signerverifier/test-data/rsa-test-key.pub b/signerverifier/test-data/rsa-test-key.pub
new file mode 100644
index 0000000..3482484
--- /dev/null
+++ b/signerverifier/test-data/rsa-test-key.pub
@@ -0,0 +1,11 @@
+-----BEGIN PUBLIC KEY-----
+MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEA04egZRic+dZMVtiQc56D
+ejU4FF1q3aOkUKnD+Q4lTbj1zp6ODKJTcktupmrad68jqtMiSGG8he6ELFs377q8
+bbgEUMWgAf+06Q8oFvUSfOXzZNFI7H5SMPOJY5aDWIMIEZ8DlcO7TfkA7D3iAEJX
+xxTOVS3UAIk5umO7Y7t7yXr8O/C4u78krGazCnoblcekMLJZV4O/5BloWNAe/B1c
+vZdaZUf3brD4ZZrxEtXw/tefhn1aHsSUajVW2wwjSpKhqj7Z0XS3bDS3T95/3xsN
+6+hlS6A7rJfiWpKIRHj0vh2SXLDmmhQl1In8TD/aiycTUyWcBRHVPlYFgYPt6SaT
+VQSgMzSxC43/2fINb2fyt8SbUHJ3Ct+mzRzd/1AQikWhBdstJLxInewzjYE/sb+c
+2CmCxMPQG2BwmAWXaaumeJcXVPBlMgAcjMatM8bPByTbXpKDnQslOE7g/gswDIwn
+Em53T13mZzYUvbLJ0q3aljZVLIC3IZn3ZwA2yCWchBkVAgMBAAE=
+-----END PUBLIC KEY-----
diff --git a/signerverifier/test-data/test-ecdsa.98adf386.link b/signerverifier/test-data/test-ecdsa.98adf386.link
new file mode 100644
index 0000000..87661a4
--- /dev/null
+++ b/signerverifier/test-data/test-ecdsa.98adf386.link
@@ -0,0 +1,17 @@
+{
+ "signatures": [
+ {
+ "keyid": "98adf38602c48c5479e9a991ee3f8cbf541ee4f985e00f7a5fc4148d9a45b704",
+ "sig": "304502201fbb03c0937504182a48c66f9218bdcb2e99a07ada273e92e5e543867f98c8d7022100dbfa7bbf74fd76d76c1d08676419cba85bbd81dfb000f3ac6a786693ddc508f5"
+ }
+ ],
+ "signed": {
+ "_type": "link",
+ "byproducts": {},
+ "command": [],
+ "environment": {},
+ "materials": {},
+ "name": "test-ecdsa",
+ "products": {}
+ }
+} \ No newline at end of file
diff --git a/signerverifier/test-data/test-ed25519.52e3b8e7.link b/signerverifier/test-data/test-ed25519.52e3b8e7.link
new file mode 100644
index 0000000..f4f74e3
--- /dev/null
+++ b/signerverifier/test-data/test-ed25519.52e3b8e7.link
@@ -0,0 +1,17 @@
+{
+ "signatures": [
+ {
+ "keyid": "52e3b8e73279d6ebdd62a5016e2725ff284f569665eb92ccb145d83817a02997",
+ "sig": "4c8b7605a9195d4ddba54493bbb5257a9836c1d16056a027fd77e97b95a4f3e36f8bc3c9c9960387d68187760b3072a30c44f992c5bf8f7497c303a3b0a32403"
+ }
+ ],
+ "signed": {
+ "_type": "link",
+ "byproducts": {},
+ "command": [],
+ "environment": {},
+ "materials": {},
+ "name": "test-ed25519",
+ "products": {}
+ }
+} \ No newline at end of file
diff --git a/signerverifier/test-data/test-rsa.4e8d20af.link b/signerverifier/test-data/test-rsa.4e8d20af.link
new file mode 100644
index 0000000..4ed6b64
--- /dev/null
+++ b/signerverifier/test-data/test-rsa.4e8d20af.link
@@ -0,0 +1,17 @@
+{
+ "signatures": [
+ {
+ "keyid": "4e8d20af09fcaed6c388a186427f94a5f7ff5591ec295f4aab2cff49ffe39e9b",
+ "sig": "8958e5be66ee4352880a531bd097d1727adcc78e66b4faeb4a2cd6ad073dcb84f9a34e8156af39a7144cb5cd925325a18ccd4f0b2f981d6ff82655a7d63210d36655c50a0bf24e4839c10430a040dd6189d04fabec90eae4314c75ae2d585da17a56aaf6755e613a3a6a471ad2eddbb24504848e34f9ac163660f8ab80d7701bfa1189578a59597b3809ee62a70a7cc9545cfa65e23018fa442a45279b9fcf9d80bc92df711bfcfe16e3eae1bcf61b3286c1f0bdda17bc28bfab5b736bdcac4a38e31db1d0e0f56a2853b1b451650305f040a3425c3be47125700e92ef82c5a91a040b5e70ab7f6ebbe037ae1a6835044b5699748037e2e39a55a420c41cd9fa6e16868776367e3620e7d28eb9d8a3d710bdc98d488df1a9947d2ec8400f3c6209e8ca587cbffa30ceb3be98105e03182aab1bbb3c4e2560d99f0b09c012df2271f273ac70a6abb185abe11d559b118dca616417fa9205e74ab58e89ffd8b965da304ae9dc9cf6ffac4838b7c5375d6c2057a61cb286f06ad3b02a49c3af6178"
+ }
+ ],
+ "signed": {
+ "_type": "link",
+ "byproducts": {},
+ "command": [],
+ "environment": {},
+ "materials": {},
+ "name": "test-rsa",
+ "products": {}
+ }
+} \ No newline at end of file
diff --git a/signerverifier/utils.go b/signerverifier/utils.go
new file mode 100644
index 0000000..e77e07f
--- /dev/null
+++ b/signerverifier/utils.go
@@ -0,0 +1,149 @@
+package signerverifier
+
+import (
+ "crypto/sha256"
+ "crypto/x509"
+ "encoding/hex"
+ "encoding/json"
+ "encoding/pem"
+ "errors"
+ "hash"
+ "testing"
+
+ "github.com/secure-systems-lab/go-securesystemslib/cjson"
+)
+
+/*
+Credits: Parts of this file were originally authored for in-toto-golang.
+*/
+
+var (
+ // ErrNoPEMBlock gets triggered when there is no PEM block in the provided file
+ ErrNoPEMBlock = errors.New("failed to decode the data as PEM block (are you sure this is a pem file?)")
+ // ErrFailedPEMParsing gets returned when PKCS1, PKCS8 or PKIX key parsing fails
+ ErrFailedPEMParsing = errors.New("failed parsing the PEM block: unsupported PEM type")
+)
+
+// LoadKeyFromSSLibBytes returns a pointer to a Key instance created from the
+// contents of the bytes. The key contents are expected to be in the custom
+// securesystemslib format.
+func LoadKeyFromSSLibBytes(contents []byte) (*SSLibKey, error) {
+ var key *SSLibKey
+ if err := json.Unmarshal(contents, &key); err != nil {
+ return LoadRSAPSSKeyFromBytes(contents)
+ }
+ if len(key.KeyID) == 0 {
+ keyID, err := calculateKeyID(key)
+ if err != nil {
+ return nil, err
+ }
+ key.KeyID = keyID
+ }
+
+ return key, nil
+}
+
+func calculateKeyID(k *SSLibKey) (string, error) {
+ key := map[string]any{
+ "keytype": k.KeyType,
+ "scheme": k.Scheme,
+ "keyid_hash_algorithms": k.KeyIDHashAlgorithms,
+ "keyval": map[string]string{
+ "public": k.KeyVal.Public,
+ },
+ }
+ canonical, err := cjson.EncodeCanonical(key)
+ if err != nil {
+ return "", err
+ }
+ digest := sha256.Sum256(canonical)
+ return hex.EncodeToString(digest[:]), nil
+}
+
+/*
+generatePEMBlock creates a PEM block from scratch via the keyBytes and the pemType.
+If successful it returns a PEM block as []byte slice. This function should always
+succeed, if keyBytes is empty the PEM block will have an empty byte block.
+Therefore only header and footer will exist.
+*/
+func generatePEMBlock(keyBytes []byte, pemType string) []byte {
+ // construct PEM block
+ pemBlock := &pem.Block{
+ Type: pemType,
+ Headers: nil,
+ Bytes: keyBytes,
+ }
+ return pem.EncodeToMemory(pemBlock)
+}
+
+/*
+decodeAndParsePEM receives potential PEM bytes decodes them via pem.Decode
+and pushes them to parseKey. If any error occurs during this process,
+the function will return nil and an error (either ErrFailedPEMParsing
+or ErrNoPEMBlock). On success it will return the decoded pemData, the
+key object interface and nil as error. We need the decoded pemData,
+because LoadKey relies on decoded pemData for operating system
+interoperability.
+*/
+func decodeAndParsePEM(pemBytes []byte) (*pem.Block, any, error) {
+ // pem.Decode returns the parsed pem block and a rest.
+ // The rest is everything, that could not be parsed as PEM block.
+ // Therefore we can drop this via using the blank identifier "_"
+ data, _ := pem.Decode(pemBytes)
+ if data == nil {
+ return nil, nil, ErrNoPEMBlock
+ }
+
+ // Try to load private key, if this fails try to load
+ // key as public key
+ key, err := parsePEMKey(data.Bytes)
+ if err != nil {
+ return nil, nil, err
+ }
+ return data, key, nil
+}
+
+/*
+parseKey tries to parse a PEM []byte slice. Using the following standards
+in the given order:
+
+ - PKCS8
+ - PKCS1
+ - PKIX
+
+On success it returns the parsed key and nil.
+On failure it returns nil and the error ErrFailedPEMParsing
+*/
+func parsePEMKey(data []byte) (any, error) {
+ key, err := x509.ParsePKCS8PrivateKey(data)
+ if err == nil {
+ return key, nil
+ }
+ key, err = x509.ParsePKCS1PrivateKey(data)
+ if err == nil {
+ return key, nil
+ }
+ key, err = x509.ParsePKIXPublicKey(data)
+ if err == nil {
+ return key, nil
+ }
+ key, err = x509.ParseECPrivateKey(data)
+ if err == nil {
+ return key, nil
+ }
+ return nil, ErrFailedPEMParsing
+}
+
+func hashBeforeSigning(data []byte, h hash.Hash) []byte {
+ h.Write(data)
+ return h.Sum(nil)
+}
+
+func hexDecode(t *testing.T, data string) []byte {
+ t.Helper()
+ b, err := hex.DecodeString(data)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return b
+}
diff --git a/signerverifier/utils_test.go b/signerverifier/utils_test.go
new file mode 100644
index 0000000..ccb22c2
--- /dev/null
+++ b/signerverifier/utils_test.go
@@ -0,0 +1,41 @@
+package signerverifier
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestLoadKeyFromSSLibBytes(t *testing.T) {
+ t.Run("RSA public key", func(t *testing.T) {
+ contents, err := os.ReadFile(filepath.Join("test-data", "rsa-test-key.pub"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ key, err := LoadKeyFromSSLibBytes(contents)
+ assert.Nil(t, err)
+
+ assert.Equal(t, "4e8d20af09fcaed6c388a186427f94a5f7ff5591ec295f4aab2cff49ffe39e9b", key.KeyID)
+ assert.Equal(t, "-----BEGIN PUBLIC KEY-----\nMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEA04egZRic+dZMVtiQc56D\nejU4FF1q3aOkUKnD+Q4lTbj1zp6ODKJTcktupmrad68jqtMiSGG8he6ELFs377q8\nbbgEUMWgAf+06Q8oFvUSfOXzZNFI7H5SMPOJY5aDWIMIEZ8DlcO7TfkA7D3iAEJX\nxxTOVS3UAIk5umO7Y7t7yXr8O/C4u78krGazCnoblcekMLJZV4O/5BloWNAe/B1c\nvZdaZUf3brD4ZZrxEtXw/tefhn1aHsSUajVW2wwjSpKhqj7Z0XS3bDS3T95/3xsN\n6+hlS6A7rJfiWpKIRHj0vh2SXLDmmhQl1In8TD/aiycTUyWcBRHVPlYFgYPt6SaT\nVQSgMzSxC43/2fINb2fyt8SbUHJ3Ct+mzRzd/1AQikWhBdstJLxInewzjYE/sb+c\n2CmCxMPQG2BwmAWXaaumeJcXVPBlMgAcjMatM8bPByTbXpKDnQslOE7g/gswDIwn\nEm53T13mZzYUvbLJ0q3aljZVLIC3IZn3ZwA2yCWchBkVAgMBAAE=\n-----END PUBLIC KEY-----", key.KeyVal.Public)
+ assert.Equal(t, RSAKeyScheme, key.Scheme)
+ assert.Equal(t, RSAKeyType, key.KeyType)
+ })
+
+ t.Run("RSA private key", func(t *testing.T) {
+ contents, err := os.ReadFile(filepath.Join("test-data", "rsa-test-key"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ key, err := LoadKeyFromSSLibBytes(contents)
+ assert.Nil(t, err)
+
+ assert.Equal(t, "4e8d20af09fcaed6c388a186427f94a5f7ff5591ec295f4aab2cff49ffe39e9b", key.KeyID)
+ assert.Equal(t, "-----BEGIN PUBLIC KEY-----\nMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEA04egZRic+dZMVtiQc56D\nejU4FF1q3aOkUKnD+Q4lTbj1zp6ODKJTcktupmrad68jqtMiSGG8he6ELFs377q8\nbbgEUMWgAf+06Q8oFvUSfOXzZNFI7H5SMPOJY5aDWIMIEZ8DlcO7TfkA7D3iAEJX\nxxTOVS3UAIk5umO7Y7t7yXr8O/C4u78krGazCnoblcekMLJZV4O/5BloWNAe/B1c\nvZdaZUf3brD4ZZrxEtXw/tefhn1aHsSUajVW2wwjSpKhqj7Z0XS3bDS3T95/3xsN\n6+hlS6A7rJfiWpKIRHj0vh2SXLDmmhQl1In8TD/aiycTUyWcBRHVPlYFgYPt6SaT\nVQSgMzSxC43/2fINb2fyt8SbUHJ3Ct+mzRzd/1AQikWhBdstJLxInewzjYE/sb+c\n2CmCxMPQG2BwmAWXaaumeJcXVPBlMgAcjMatM8bPByTbXpKDnQslOE7g/gswDIwn\nEm53T13mZzYUvbLJ0q3aljZVLIC3IZn3ZwA2yCWchBkVAgMBAAE=\n-----END PUBLIC KEY-----", key.KeyVal.Public)
+ expectedPrivateKey := "-----BEGIN RSA PRIVATE KEY-----\n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\n-----END RSA PRIVATE KEY-----"
+ assert.Equal(t, expectedPrivateKey, key.KeyVal.Private)
+ assert.Equal(t, RSAKeyScheme, key.Scheme)
+ assert.Equal(t, RSAKeyType, key.KeyType)
+ })
+}