From 3d4ba0d0807b618fb0d5f1bc6c17a6774f6c9145 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Tue, 16 Apr 2024 19:40:08 +0200 Subject: Adding upstream version 0.8.0. Signed-off-by: Daniel Baumann --- .github/dependabot.yml | 22 ++ .github/workflows/build.yml | 30 ++ LICENSE | 21 ++ README.md | 2 + cjson/canonicaljson.go | 151 ++++++++ cjson/canonicaljson_test.go | 142 ++++++++ dsse/envelope.go | 64 ++++ dsse/sign.go | 85 +++++ dsse/sign_test.go | 380 ++++++++++++++++++++ dsse/signerverifier.go | 43 +++ dsse/verify.go | 138 ++++++++ dsse/verify_test.go | 382 +++++++++++++++++++++ encrypted/encrypted.go | 290 ++++++++++++++++ encrypted/encrypted_test.go | 150 ++++++++ go.mod | 16 + go.sum | 17 + signerverifier/ecdsa.go | 111 ++++++ signerverifier/ecdsa_test.go | 191 +++++++++++ signerverifier/ed25519.go | 98 ++++++ signerverifier/ed25519_test.go | 207 +++++++++++ signerverifier/rsa.go | 161 +++++++++ signerverifier/rsa_test.go | 193 +++++++++++ signerverifier/signerverifier.go | 36 ++ signerverifier/test-data/ecdsa-test-key | 1 + signerverifier/test-data/ecdsa-test-key.pub | 1 + signerverifier/test-data/ed25519-test-key | 1 + signerverifier/test-data/ed25519-test-key.pub | 1 + signerverifier/test-data/rsa-test-key | 39 +++ signerverifier/test-data/rsa-test-key.pub | 11 + signerverifier/test-data/test-ecdsa.98adf386.link | 17 + .../test-data/test-ed25519.52e3b8e7.link | 17 + signerverifier/test-data/test-rsa.4e8d20af.link | 17 + signerverifier/utils.go | 149 ++++++++ signerverifier/utils_test.go | 41 +++ 34 files changed, 3225 insertions(+) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/build.yml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 cjson/canonicaljson.go create mode 100644 cjson/canonicaljson_test.go create mode 100644 dsse/envelope.go create mode 100644 dsse/sign.go create mode 100644 dsse/sign_test.go create mode 100644 dsse/signerverifier.go create mode 100644 dsse/verify.go create mode 100644 dsse/verify_test.go create mode 100644 encrypted/encrypted.go create mode 100644 encrypted/encrypted_test.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 signerverifier/ecdsa.go create mode 100644 signerverifier/ecdsa_test.go create mode 100644 signerverifier/ed25519.go create mode 100644 signerverifier/ed25519_test.go create mode 100644 signerverifier/rsa.go create mode 100644 signerverifier/rsa_test.go create mode 100644 signerverifier/signerverifier.go create mode 100644 signerverifier/test-data/ecdsa-test-key create mode 100755 signerverifier/test-data/ecdsa-test-key.pub create mode 100644 signerverifier/test-data/ed25519-test-key create mode 100644 signerverifier/test-data/ed25519-test-key.pub create mode 100644 signerverifier/test-data/rsa-test-key create mode 100644 signerverifier/test-data/rsa-test-key.pub create mode 100644 signerverifier/test-data/test-ecdsa.98adf386.link create mode 100644 signerverifier/test-data/test-ed25519.52e3b8e7.link create mode 100644 signerverifier/test-data/test-rsa.4e8d20af.link create mode 100644 signerverifier/utils.go create mode 100644 signerverifier/utils_test.go 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-----\nMIIG5AIBAAKCAYEA04egZRic+dZMVtiQc56DejU4FF1q3aOkUKnD+Q4lTbj1zp6O\nDKJTcktupmrad68jqtMiSGG8he6ELFs377q8bbgEUMWgAf+06Q8oFvUSfOXzZNFI\n7H5SMPOJY5aDWIMIEZ8DlcO7TfkA7D3iAEJXxxTOVS3UAIk5umO7Y7t7yXr8O/C4\nu78krGazCnoblcekMLJZV4O/5BloWNAe/B1cvZdaZUf3brD4ZZrxEtXw/tefhn1a\nHsSUajVW2wwjSpKhqj7Z0XS3bDS3T95/3xsN6+hlS6A7rJfiWpKIRHj0vh2SXLDm\nmhQl1In8TD/aiycTUyWcBRHVPlYFgYPt6SaTVQSgMzSxC43/2fINb2fyt8SbUHJ3\nCt+mzRzd/1AQikWhBdstJLxInewzjYE/sb+c2CmCxMPQG2BwmAWXaaumeJcXVPBl\nMgAcjMatM8bPByTbXpKDnQslOE7g/gswDIwnEm53T13mZzYUvbLJ0q3aljZVLIC3\nIZn3ZwA2yCWchBkVAgMBAAECggGAKswAeCPMMsIYTOPhCftyt2mIEJq78d7Xclh+\npWemxXxcAzNSIx0+i9vWJcZtsBRXv4qbH5DiryhMRpsoDJE36Wz3No5darodFKAz\n6L0pwepWXbn4Kpz+LRhA3kzIA0LzgXkuJQFmZoawGJwGmy3RC57ahiJRB9C7xMnD\n0pBOobuHx+rSvW2VUmou5DpDVYEAZ7fV2p511wUK9xkYg8K/Dj7Ok7pFRfh5MTlx\nd/GgIjdm97Np5dq4+moTShtBEqfqviv1OfDa32DISAOcEKiC2jg0O96khDz2YjK4\n0HAbWrGjVB1v+/kWKTWJ6/ddLb+Dk77KKeZ4pSPKYeUM7jXlyVikntmFTw4CXFvk\n2QqOfJyBxAxcx4eB/n6j1mqIvqL6TjloXn/Bhc/65Fr5een3hLbRnhtNxXBURwVo\nYYJwLw7tZOMKqt51qbKU2XqaII7iVHGPaeDUYs4PaBSSW/E1FFAZbId1GSe4+mDi\nJipxs4M6S9N9FPgTmZlgQ/0j6VMhAoHBANrygq2IsgRjczVO+FhOAmmP6xjbcoII\n582JTunwb8Yf4KJR8DM295LRcafk9Ns4l3QF/rESK8mZAbMUsjKlD4WcE2QTOEoQ\nQBV+lJLDyYeAhmq2684dqaIGA5jEW0GcfDpj42Hhy/qiy1PWTe/O1aFaLaYV0bXL\nPN1CTGpc+DdRh5lX7ftoTS/Do0U9Of30s00Bm9AV0LLoyH5WmXpGWatOYBHHwomi\n08vMsbJelgFzDQPRjHfpj7+EZh1wdqe8cQKBwQD3U8QP7ZatB5ymMLsefm/I6Uor\nwz5SqMyiz+u/Fc+4Ii8SwLsVQw+IoZyxofkKTbMESrgQhLbzC59eRbUcF7GZ+lZQ\nw6gG/+YLvx9MYcEVGeruyPmlYFp6g+vN/qEiPs1oZej8r1XjNj228XdTMAJ2qTbZ\nGVyhEMMbBgd5FFxEqueD5/EILT6xj9BxvQ1m2IFbVIkXfOrhdwEk+RcbXDA0n+rS\nkhBajWQ3eVQGY2hWnYB+1fmumYFs8hAaMAJlCOUCgcBCvi6Ly+HIaLCUDZCzCoS9\nvTuDhlHvxdsz0qmVss+/67PEh4nbcuQhg2tMLQVfVm8E1VcAj3N9rwDPoH155stG\nhX97wEgme7GtW7rayohCoDFZko1rdatiUscB6MmQxK0x94U3L2fI7Zth4TA87CY/\nW4gS2w/khSH2qOE2g0S/SEE3w5AuVWtCJjc9Qh7NhayqytS+qAfIoiGMMcXzekKX\nb/rlMKni3xoFRE7e+uprYrES+uwBGdfSIAAo9UGWfGECgcEA8pCJ4qE+vJaRkQCM\nFD0mvyHl54PGFOWORUOsTy1CGrIT/s1c7l5l1rfB6QkVKYDIyLXLThALKdVFSP0O\nwe2O9pfpna42lh7VbMHWHWBmMJ7JpcUf6ozUUAIf+1j2iZKUfAYu+duwXXWuE0VA\npSqZz+znaQaRrTm2UEOagqpwT7xZ8SlCYKWXLigA4/vpL+u4+myvQ4T1C4leaveN\nLP0+He6VLE2qklTHbAynVtiZ1REFm9+Z0B6nK8U/+58ISjTtAoHBALgqMopFIOMw\nAhhasnrL3Pzxf0WKzKmj/y2yEP0Vctm0muqxFnFwPwyOAd6HODJOSiFPD5VN4jvC\n+Yw96Qn29kHGXTKgL1J9cSL8z6Qzlc+UYCdSwmaZK5r36+NBTJgvKY9KrpkXCkSa\nc5YgIYtXMitmq9NmNvcSJWmuuiept3HFlwkU3pfmwzKNEeqi2jmuIOqI2zCOqX67\nI+YQsJgrHE0TmYxxRkgeYUy7s5DoHE25rfvdy5Lx+xAOH8ZgD1SGOw==\n-----END RSA PRIVATE KEY-----" + assert.Equal(t, expectedPrivateKey, key.KeyVal.Private) + assert.Equal(t, RSAKeyScheme, key.Scheme) + assert.Equal(t, RSAKeyType, key.KeyType) + }) +} -- cgit v1.2.3