From fd96547a038483748066ac51d81b6c515db88df1 Mon Sep 17 00:00:00 2001 From: Greg Guthe Date: Tue, 28 Sep 2021 15:03:07 -0400 Subject: [PATCH 01/11] formats: add files to sig request --- formats/rest.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/formats/rest.go b/formats/rest.go index 5e45e0309..56bed608a 100644 --- a/formats/rest.go +++ b/formats/rest.go @@ -1,9 +1,18 @@ package formats +// SigningFile is a file to sign when included in a request to sign +// multiple files or a signed file when included in a response to +// signing multiple files +type SigningFile struct { + Name string `json:"name"` + Content string `json:"content"` +} + // SignatureRequest is sent by a client to request a signature on input data type SignatureRequest struct { - Input string `json:"input"` - KeyID string `json:"keyid,omitempty"` + Input string `json:"input"` + Files []SigningFile `json:"files,omitempty"` + KeyID string `json:"keyid,omitempty"` Options interface{} } From 1638ccdc806ca0003581f266884af11ff79a8e15 Mon Sep 17 00:00:00 2001 From: Greg Guthe Date: Tue, 28 Sep 2021 15:03:31 -0400 Subject: [PATCH 02/11] formats: add signed_files to sig response --- formats/rest.go | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/formats/rest.go b/formats/rest.go index 56bed608a..2629ecddc 100644 --- a/formats/rest.go +++ b/formats/rest.go @@ -19,13 +19,14 @@ type SignatureRequest struct { // SignatureResponse is returned by autograph to a client with // a signature computed on input data type SignatureResponse struct { - Ref string `json:"ref"` - Type string `json:"type"` - Mode string `json:"mode"` - SignerID string `json:"signer_id"` - PublicKey string `json:"public_key"` - Signature string `json:"signature,omitempty"` - SignedFile string `json:"signed_file,omitempty"` - X5U string `json:"x5u,omitempty"` - SignerOpts interface{} `json:"signer_opts,omitempty"` + Ref string `json:"ref"` + Type string `json:"type"` + Mode string `json:"mode"` + SignerID string `json:"signer_id"` + PublicKey string `json:"public_key"` + Signature string `json:"signature,omitempty"` + SignedFile string `json:"signed_file,omitempty"` + SignedFiles []SigningFile `json:"signed_files,omitempty"` + X5U string `json:"x5u,omitempty"` + SignerOpts interface{} `json:"signer_opts,omitempty"` } From 322fd9c741440c7ffa4a1128f17e709237ec76c2 Mon Sep 17 00:00:00 2001 From: Greg Guthe Date: Mon, 27 Sep 2021 15:26:19 -0400 Subject: [PATCH 03/11] add /sign/files handler --- docs/endpoints.md | 90 +++++++++++++++++++++++++++++++ handlers.go | 134 ++++++++++++++++++++++++++++++++++------------ handlers_test.go | 99 +++++++++++++++++++++++++--------- main.go | 1 + signer/signer.go | 60 +++++++++++++++++++++ 5 files changed, 325 insertions(+), 59 deletions(-) diff --git a/docs/endpoints.md b/docs/endpoints.md index 170070dec..f126138fb 100644 --- a/docs/endpoints.md +++ b/docs/endpoints.md @@ -82,6 +82,96 @@ Each signature response contains the following fields: format. Each signer uses a different format, so refer to their documentation for more information. +## /sign/files + +### Request + +Request to sign multiple files. The files to sign are passed in +the request body using the JSON format described below. + +The request body is an array of signature requests, to allow for +batching signatures into a single API request. The parameters are: + +- **keyid**: allows the caller to specify a key to sign the data with. + This parameter is optional, and Autograph will pick a key based on + the caller\'s permission if omitted. +- **options**: a JSON object used to pass signer-specific options in + the request. Refer to the documentation of each signer to find out + which options they accept. +- **files**: an array of dicts of file name and base64 encoded content to sign. For example: + +```json +[ + { + "content": "UEsDBBQACAAIAAAAAAAAAAAAAAAAAAAAAAATAAAAQW5kcm9pZE1hbmlmZXN0LnhtbKSYS2ybx7XHf0PqbVmW4...BwAACigAAAAA", + "name": "sphinx_1.7.2-1.dsc" + }, + { + "content": "UEsDBBQACAAIAAAAAAAAAAAAAAAAAAAAAAATAAAAQW5kcm9pZE1hbmlmZXN0LnhtbKSYS2ybx7XHf0PqbVmW4...BwAACigAAAAA", + "name": "sphinx_1.7.2-1.changes" + } +] +``` + +The number of files must be between 1 and 32 inclusive. All characters +in a file name must be an alphanumeric character, dash, underscore or +dot. The file name must start with an alphanumeric character and dots +cannot occur next to each other in the file name. + +example: + +``` bash +POST /sign/file +Host: autograph.example.net +Content-type: application/json +Authorization: Hawk id="alice", mac="756lSgQEYLoc6V0Uv2wS8pRg/h+4WFUVKWQynCFvY8Y=", ts="1524487134", nonce="MrpGL35q", hash="9m3WhtGQDuHermi5fDYBGJlOqNeK5B3nk0lKreZ+YSw=", ext="933126753" + +[ + { + "files":[ + { + "content": "UEsDBBQACAAIAAAAAAAAAAAAAAAAAAAAAAATAAAAQW5kcm9pZE1hbmlmZXN0LnhtbKSYS2ybx7XHf0PqbVmW4...BwAACigAAAAA", + "name": "sphinx_1.7.2-1.dsc" + }, + { + "content": "UEsDBBQACAAIAAAAAAAAAAAAAAAAAAAAAAATAAAAQW5kcm9pZE1hbmlmZXN0LnhtbKSYS2ybx7XHf0PqbVmW4...BwAACigAAAAA", + "name": "sphinx_1.7.2-1.changes" + } + ] + }, + { + "files":[ + { + "content": "UEsDBBQACAAIAAAAAAAAAAAAAAAAAAAAAAATAAAAQW5kcm9pZE1hbmlmZXN0LnhtbKSYS2ybx7XHf0PqbVmW4...BwAACigAAAAA", + "name": "sphinx_1.7.2-1.dsc" + }, + { + "content": "UEsDBBQACAAIAAAAAAAAAAAAAAAAAAAAAAATAAAAQW5kcm9pZE1hbmlmZXN0LnhtbKSYS2ybx7XHf0PqbVmW4...BwAACigAAAAA", + "name": "sphinx_1.7.2-1.changes" + }, + ], + "keyid":"randompgp-debsign", + "options":null + } +] +``` + +### Response + +A successful request returns `201 Created` with a response body +containing all signed files encoded in JSON. The ordering of the +response array is identical to the request array, such that signing +request 0 maps to signing response 0, etc. + +The response format is the same as `/sign/data` except +instead of the `signature` field autograph returns the +field: + +- `signed_files` an array of dicts of file name and base64 encoded + signed file content as described in the request section. Each + signer uses a different format, so refer to their documentation + for more information. + ## /sign/file ### Request diff --git a/handlers.go b/handlers.go index 408eb4f1b..102409930 100644 --- a/handlers.go +++ b/handlers.go @@ -25,6 +25,16 @@ import ( log "github.com/sirupsen/logrus" ) +const ( + // MinNamedFiles is the minimum number of named files a single + // multi-file signing request can include + MinNamedFiles = 1 + + // MaxNamedFiles is the maximum number of named files a single + // multi-file signing request can include + MaxNamedFiles = 32 +) + // heartbeatConfig configures the heartbeat handler. It sets timeouts // for each backing service to check. // @@ -47,18 +57,20 @@ func hashSHA256AsHex(toHash []byte) string { return fmt.Sprintf("%X", h.Sum(nil)) } -func logSigningRequestFailure(sigreq formats.SignatureRequest, sigresp formats.SignatureResponse, rid, userid, inputHash string, starttime time.Time, err error) { +func logSigningRequestFailure(sigreq formats.SignatureRequest, sigresp formats.SignatureResponse, rid, userid, inputHash string, inputHashes []string, starttime time.Time, err error) { log.WithFields(log.Fields{ - "rid": rid, - "options": sigreq.Options, - "mode": sigresp.Mode, - "ref": sigresp.Ref, - "type": sigresp.Type, - "signer_id": sigresp.SignerID, - "input_hash": inputHash, - "output_hash": nil, - "user_id": userid, - "t": int32(time.Since(starttime) / time.Millisecond), // request processing time in ms + "rid": rid, + "options": sigreq.Options, + "mode": sigresp.Mode, + "ref": sigresp.Ref, + "type": sigresp.Type, + "signer_id": sigresp.SignerID, + "input_hash": inputHash, + "input_hashes": inputHashes, + "output_hash": nil, + "output_hashes": nil, + "user_id": userid, + "t": int32(time.Since(starttime) / time.Millisecond), // request processing time in ms }).Info(fmt.Sprintf("signing operation failed with error: %v", err)) } @@ -121,7 +133,21 @@ func (a *autographer) handleSignature(w http.ResponseWriter, r *http.Request) { return } for i, sigreq := range sigreqs { - if sigreq.Input == "" { + if r.URL.RequestURI() == "/sign/files" { + if sigreq.Input != "" { + httpError(w, r, http.StatusBadRequest, fmt.Sprintf("input should be empty in sign files signature request %d", i)) + } + if sigreq.Files == nil { + httpError(w, r, http.StatusBadRequest, fmt.Sprintf("missing Files in sign files signature request %d", i)) + } + if len(sigreq.Files) < MinNamedFiles { + httpError(w, r, http.StatusBadRequest, "Did not receive enough files to sign. Need at least %d", MinNamedFiles) + return + } else if len(sigreq.Files) > MaxNamedFiles { + httpError(w, r, http.StatusBadRequest, "Received too many files to sign (max is %d)", MaxNamedFiles) + return + } + } else if sigreq.Input == "" { httpError(w, r, http.StatusBadRequest, fmt.Sprintf("missing input in signature request %d", i)) } } @@ -135,17 +161,34 @@ func (a *autographer) handleSignature(w http.ResponseWriter, r *http.Request) { // the signature is then encoded appropriately, and added to the response slice for i, sigreq := range sigreqs { var ( - input []byte - sig signer.Signature - signedfile []byte - inputHash, outputHash string + input []byte + unsignedNamedFiles []signer.NamedUnsignedFile + sig signer.Signature + signedfile []byte + signedfiles []signer.NamedSignedFile + inputHash, outputHash string + inputHashes, outputHashes []string ) - // Decode the base64 input data - input, err = base64.StdEncoding.DecodeString(sigreq.Input) - if err != nil { - httpError(w, r, http.StatusBadRequest, "%v", err) - return + if r.URL.RequestURI() == "/sign/files" { + for i, inputFile := range sigreq.Files { + log.Debugf("base64 decoding file %d", i) + unsignedNamedFile, err := signer.NewNamedUnsignedFile(inputFile) + if err != nil { + httpError(w, r, http.StatusBadRequest, "%q", err) + return + } + log.Debugf("base64 decoded unsigned named file %d: %s", i, unsignedNamedFile.Name) + unsignedNamedFiles = append(unsignedNamedFiles, *unsignedNamedFile) + } + log.Debugf("signing %d unsigned named files", len(unsignedNamedFiles)) + } else { + // Decode the base64 input data + input, err = base64.StdEncoding.DecodeString(sigreq.Input) + if err != nil { + httpError(w, r, http.StatusBadRequest, "%v", err) + return + } } // returns an error if the signer is not found or if @@ -179,7 +222,7 @@ func (a *autographer) handleSignature(w http.ResponseWriter, r *http.Request) { sig, err = hashSigner.SignHash(input, sigreq.Options) if err != nil { - logSigningRequestFailure(sigreq, sigresps[i], rid, userid, inputHash, starttime, err) + logSigningRequestFailure(sigreq, sigresps[i], rid, userid, inputHash, inputHashes, starttime, err) httpError(w, r, http.StatusInternalServerError, "signing request %s failed with error: %v", sigresps[i].Ref, err) return } @@ -200,7 +243,7 @@ func (a *autographer) handleSignature(w http.ResponseWriter, r *http.Request) { sig, err = dataSigner.SignData(input, sigreq.Options) if err != nil { - logSigningRequestFailure(sigreq, sigresps[i], rid, userid, inputHash, starttime, err) + logSigningRequestFailure(sigreq, sigresps[i], rid, userid, inputHash, inputHashes, starttime, err) httpError(w, r, http.StatusInternalServerError, "signing request %s failed with error: %v", sigresps[i].Ref, err) return } @@ -221,24 +264,47 @@ func (a *autographer) handleSignature(w http.ResponseWriter, r *http.Request) { signedfile, err = fileSigner.SignFile(input, sigreq.Options) if err != nil { - logSigningRequestFailure(sigreq, sigresps[i], rid, userid, inputHash, starttime, err) + logSigningRequestFailure(sigreq, sigresps[i], rid, userid, inputHash, inputHashes, starttime, err) httpError(w, r, http.StatusInternalServerError, "signing request %s failed with error: %v", sigresps[i].Ref, err) return } sigresps[i].SignedFile = base64.StdEncoding.EncodeToString(signedfile) outputHash = hashSHA256AsHex(signedfile) + case "/sign/files": + multiFileSigner, ok := requestedSigner.(signer.MultipleFileSigner) + if !ok { + httpError(w, r, http.StatusBadRequest, "requested signer %q does not implement multiple file signing", requestedSignerConfig.ID) + return + } + // calculate a hash of the input files to log + for _, inputFile := range unsignedNamedFiles { + inputHashes = append(inputHashes, hashSHA256AsHex(inputFile.Bytes)) + } + + signedfiles, err = multiFileSigner.SignFiles(unsignedNamedFiles, sigreq.Options) + if err != nil { + logSigningRequestFailure(sigreq, sigresps[i], rid, userid, inputHash, inputHashes, starttime, err) + httpError(w, r, http.StatusInternalServerError, "signing request %s failed with error: %v", sigresps[i].Ref, err) + return + } + for _, signedFile := range signedfiles { + outputHashes = append(outputHashes, hashSHA256AsHex(signedFile.Bytes)) + sigresps[i].SignedFiles = append(sigresps[i].SignedFiles, *signedFile.RESTSigningFile()) + } } log.WithFields(log.Fields{ - "rid": rid, - "options": sigreq.Options, - "mode": sigresps[i].Mode, - "ref": sigresps[i].Ref, - "type": sigresps[i].Type, - "signer_id": sigresps[i].SignerID, - "input_hash": inputHash, - "output_hash": outputHash, - "user_id": userid, - "t": int32(time.Since(starttime) / time.Millisecond), // request processing time in ms + "rid": rid, + "options": sigreq.Options, + "mode": sigresps[i].Mode, + "ref": sigresps[i].Ref, + "type": sigresps[i].Type, + "signer_id": sigresps[i].SignerID, + "input_hash": inputHash, + "input_hashes": inputHashes, + "output_hash": outputHash, + "output_hashes": outputHashes, + "user_id": userid, + "t": int32(time.Since(starttime) / time.Millisecond), // request processing time in ms }).Info("signing operation succeeded") } respdata, err := json.Marshal(sigresps) diff --git a/handlers_test.go b/handlers_test.go index 40e0704e3..4ce61b0b2 100644 --- a/handlers_test.go +++ b/handlers_test.go @@ -50,42 +50,91 @@ func TestBadRequest(t *testing.T) { // missing request body {`/sign/data`, `POST`, ``}, {`/sign/hash`, `POST`, ``}, + {`/sign/file`, `POST`, ``}, + {`/sign/files`, `POST`, ``}, // invalid json body {`/sign/data`, `POST`, `{|||...........`}, {`/sign/hash`, `POST`, `{|||...........`}, + {`/sign/file`, `POST`, `{|||...........`}, + {`/sign/files`, `POST`, `{|||...........`}, // missing input - {`/sign/data`, `POST`, `[{"input": "", "keyid": "abcd"}]`}, - {`/sign/hash`, `POST`, `[{"input": "", "keyid": "abcd"}]`}, + {`/sign/data`, `POST`, `[{"input": ""}]`}, + {`/sign/hash`, `POST`, `[{"input": ""}]`}, + {`/sign/file`, `POST`, `[{"input": ""}]`}, + {`/sign/files`, `POST`, `[{"input": ""}]`}, // input not in base64 {`/sign/data`, `POST`, `[{"input": "......."}]`}, {`/sign/hash`, `POST`, `[{"input": "......."}]`}, - // asking for a xpi signature using a hash will fail + {`/sign/file`, `POST`, `[{"input": "......."}]`}, + {`/sign/files`, `POST`, `[{"input": "......."}]`}, + + // missing files + {`/sign/files`, `POST`, `[{"input": "aGVsbG8=", "keyid": "randompgp-debsign"}]`}, + // files is an empty string + {`/sign/files`, `POST`, `[{"files": "", "keyid": "randompgp-debsign"}]`}, + // files is a base64 string + {`/sign/files`, `POST`, `[{"files": "aGVsbG8=", "keyid": "randompgp-debsign"}]`}, + // files is an empty array + {`/sign/files`, `POST`, `[{"files": [], "keyid": "randompgp-debsign"}]`}, + // files content is not valid base64 + {`/sign/files`, `POST`, `[{"files": [{"name": "0", "content":"...."}], "keyid": "randompgp-debsign"}]`}, + // file name includes relative current directory: ./foo.dsc + {`/sign/files`, `POST`, `[{"files": [{"name": "./foo.dsc", "content":"aGVsbG8="}], "keyid": "randompgp-debsign"}]`}, + // file name includes relative parent directory: ../../foo.dsc + {`/sign/files`, `POST`, `[{"files": [{"name": "../../foo.dsc", "content":"aGVsbG8="}], "keyid": "randompgp-debsign"}]`}, + // file name includes relative parent directory following a filename: cwd/../../foo.dsc + {`/sign/files`, `POST`, `[{"files": [{"name": "cwd/../../foo.dsc", "content":"aGVsbG8="}], "keyid": "randompgp-debsign"}]`}, + // file name includes two dots with otherwise valid chars: cwd..foo.dsc + {`/sign/files`, `POST`, `[{"files": [{"name": "cwd..foo.dsc", "content":"aGVsbG8="}], "keyid": "randompgp-debsign"}]`}, + // file name starts with dot: .bashrc.dsc + {`/sign/files`, `POST`, `[{"files": [{"name": ".bashrc.dsc", "content":"aGVsbG8="}], "keyid": "randompgp-debsign"}]`}, + // file name starts with /: /etc + {`/sign/files`, `POST`, `[{"files": [{"name": "/etc", "content":"aGVsbG8="}], "keyid": "randompgp-debsign"}]`}, + // file name is path spam/eggs/foo.dsc + {`/sign/files`, `POST`, `[{"files": [{"name": "spam/eggs/foo.dsc", "content":"aGVsbG8="}], "keyid": "randompgp-debsign"}]`}, + // file name is windows path + {`/sign/files`, `POST`, `[{"files": [{"name": "C:\spam\eggs\foo.dsc", "content":"aGVsbG8="}], "keyid": "randompgp-debsign"}]`}, + // file name is >255 chars (404 long) + {`/sign/files`, `POST`, `[{"files": [{"name": "spamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspam.dsc", "content":"aGVsbG8="}], "keyid": "randompgp-debsign"}]`}, + + // files has too many files (33) + {`/sign/files`, `POST`, `[{"files": [{"name": "0", "content":"aGVsbG8="}, {"name": "1", "content":"aGVsbG8="}, {"name": "2", "content":"aGVsbG8="}, {"name": "3", "content":"aGVsbG8="}, {"name": "4", "content":"aGVsbG8="}, {"name": "5", "content":"aGVsbG8="}, {"name": "6", "content":"aGVsbG8="}, {"name": "7", "content":"aGVsbG8="}, {"name": "8", "content":"aGVsbG8="}, {"name": "9", "content":"aGVsbG8="}, {"name": "10", "content":"aGVsbG8="}, {"name": "11", "content":"aGVsbG8="}, {"name": "12", "content":"aGVsbG8="}, {"name": "13", "content":"aGVsbG8="}, {"name": "14", "content":"aGVsbG8="}, {"name": "15", "content":"aGVsbG8="}, {"name": "16", "content":"aGVsbG8="}, {"name": "17", "content":"aGVsbG8="}, {"name": "18", "content":"aGVsbG8="}, {"name": "19", "content":"aGVsbG8="}, {"name": "20", "content":"aGVsbG8="}, {"name": "21", "content":"aGVsbG8="}, {"name": "22", "content":"aGVsbG8="}, {"name": "23", "content":"aGVsbG8="}, {"name": "24", "content":"aGVsbG8="}, {"name": "25", "content":"aGVsbG8="}, {"name": "26", "content":"aGVsbG8="}, {"name": "27", "content":"aGVsbG8="}, {"name": "28", "content":"aGVsbG8="}, {"name": "29", "content":"aGVsbG8="}, {"name": "30", "content":"aGVsbG8="}, {"name": "31", "content":"aGVsbG8="}, {"name": "32", "content":"aGVsbG8="}, {"name": "33", "content":"aGVsbG8="}], "keyid": "randompgp-debsign"}]`}, + + // asking for a xpi signature using /sign/hash fails {`/sign/hash`, `POST`, `[{"input": "Y2FyaWJvdW1hdXJpY2UK", "keyid": "webextensions-rsa"}]`}, } for i, testcase := range TESTCASES { - body := strings.NewReader(testcase.body) - req, err := http.NewRequest(testcase.method, "http://foo.bar"+testcase.endpoint, body) - if err != nil { - t.Fatal(err) - } - req.Header.Set("Content-Type", "application/json") - auth, err := ag.getAuthByID(conf.Authorizations[0].ID) - if err != nil { - t.Fatal(err) - } + i := i + testcase := testcase - authheader := getAuthHeader(req, - auth.ID, - auth.Key, - sha256.New, id(), - "application/json", - []byte(testcase.body)) - req.Header.Set("Authorization", authheader) - w := httptest.NewRecorder() - ag.handleSignature(w, req) - if w.Code == http.StatusCreated { - t.Fatalf("test case %d should have failed, but succeeded with %d: %s", i, w.Code, w.Body.String()) - } + t.Run(fmt.Sprintf("returns 400 for invalid %s %s %s", testcase.method, testcase.endpoint, testcase.body), func(t *testing.T) { + t.Parallel() + + body := strings.NewReader(testcase.body) + req, err := http.NewRequest(testcase.method, "http://foo.bar"+testcase.endpoint, body) + if err != nil { + t.Fatal(err) + } + req.Header.Set("Content-Type", "application/json") + auth, err := ag.getAuthByID(conf.Authorizations[0].ID) + if err != nil { + t.Fatal(err) + } + + authheader := getAuthHeader(req, + auth.ID, + auth.Key, + sha256.New, id(), + "application/json", + []byte(testcase.body)) + req.Header.Set("Authorization", authheader) + w := httptest.NewRecorder() + ag.handleSignature(w, req) + if w.Code != http.StatusBadRequest { + t.Fatalf("test case %d %s %s %q should have failed, but succeeded with %d: %s", i, testcase.method, testcase.endpoint, testcase.body, w.Code, w.Body.String()) + } + // t.Logf("failed with %d: %s", w.Code, w.Body.String()) + }) } } diff --git a/main.go b/main.go index dc36ca31b..b334f260e 100755 --- a/main.go +++ b/main.go @@ -210,6 +210,7 @@ func run(conf configuration, listen string, debug bool) { router.HandleFunc("/__lbheartbeat__", handleLBHeartbeat).Methods("GET") router.HandleFunc("/__version__", handleVersion).Methods("GET") router.HandleFunc("/__monitor__", monitor.handleMonitor).Methods("GET") + router.HandleFunc("/sign/files", ag.handleSignature).Methods("POST") router.HandleFunc("/sign/file", ag.handleSignature).Methods("POST") router.HandleFunc("/sign/data", ag.handleSignature).Methods("POST") router.HandleFunc("/sign/hash", ag.handleSignature).Methods("POST") diff --git a/signer/signer.go b/signer/signer.go index db4c58a51..519bfc7d3 100644 --- a/signer/signer.go +++ b/signer/signer.go @@ -22,6 +22,7 @@ import ( "time" "github.com/mozilla-services/autograph/database" + "github.com/mozilla-services/autograph/formats" "github.com/DataDog/datadog-go/statsd" "github.com/ThalesIgnite/crypto11" @@ -186,6 +187,13 @@ type FileSigner interface { GetDefaultOptions() interface{} } +// MultipleFileSigner is an interface to a signer that signs multiple +// files in one signing operation +type MultipleFileSigner interface { + SignFiles(files []NamedUnsignedFile, options interface{}) ([]NamedSignedFile, error) + GetDefaultOptions() interface{} +} + // Signature is an interface to a digital signature type Signature interface { Marshal() (signature string, err error) @@ -194,6 +202,58 @@ type Signature interface { // SignedFile is an []bytes that contains file data type SignedFile []byte +type namedFile struct { + Name string + Bytes []byte +} + +// NamedUnsignedFile is a file with a name to sign +type NamedUnsignedFile namedFile + +// NamedSignedFile is a file with a name that's been signed +type NamedSignedFile namedFile + +// isValidUnsignedFilename +func isValidUnsignedFilename(filename string) error { + if !regexp.MustCompile(`^[a-zA-z0-9]`).MatchString(filename) { + return fmt.Errorf("unsigned filename must start with an alphanumeric character") + } + if !regexp.MustCompile(`^[-_\.a-zA-Z0-9]{1,256}$`).MatchString(filename) { + return fmt.Errorf(`unsigned filename must match ^[-_\.a-zA-Z0-9]{1,256}$`) + } + if regexp.MustCompile(`\.\.`).MatchString(filename) { + return fmt.Errorf("unsigned filename must not include ..") + } + return nil +} + +// NewNamedUnsignedFile allocates and returns a ref to a new +// NamedUnsignedFile from a REST format SigningFile. It base64 decodes +// the REST SigningFile.Content into NamedUnsignedFile.Bytes. +func NewNamedUnsignedFile(restSigningFile formats.SigningFile) (*NamedUnsignedFile, error) { + if err := isValidUnsignedFilename(restSigningFile.Name); err != nil { + return nil, fmt.Errorf("invalid named file name: %w", err) + } + fileBytes, err := base64.StdEncoding.DecodeString(restSigningFile.Content) + if err != nil { + return nil, err + } + return &NamedUnsignedFile{ + Name: restSigningFile.Name, + Bytes: fileBytes, + }, nil +} + +// RESTSigningFile allocates and returns a ref to a new REST +// SigningFile from a NamedSignedFile. It base64 encodes +// NamedSignedFile.Bytes into the REST SigningFile.Content. +func (nsf *NamedSignedFile) RESTSigningFile() *formats.SigningFile { + return &formats.SigningFile{ + Name: nsf.Name, + Content: base64.StdEncoding.EncodeToString(nsf.Bytes), + } +} + // TestFileGetter returns a test file a signer will accept in its // SignFile interface type TestFileGetter interface { From 468b21208b0c6016f25587d7590e0e29e147bec6 Mon Sep 17 00:00:00 2001 From: Greg Guthe Date: Fri, 1 Oct 2021 14:21:05 -0400 Subject: [PATCH 04/11] make: add gpg-test-clean target --- Makefile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Makefile b/Makefile index 2e0100e54..c0db65bc5 100644 --- a/Makefile +++ b/Makefile @@ -83,6 +83,10 @@ showcoverage: test generate: go generate +gpg-test-clean: + rm -rf ~/.gnupg /tmp/autograph_gpg2* + killall gpg-agent + # image build order: # # app -> {app-hsm,monitor} From e09bc6fc71e0d167774c79f0b7179d77cd8ff645 Mon Sep 17 00:00:00 2001 From: Greg Guthe Date: Thu, 9 Sep 2021 14:20:38 -0400 Subject: [PATCH 05/11] add devscripts to docker image --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index bda158f64..33621c595 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ RUN addgroup --gid 10001 app \ echo 'deb http://deb.debian.org/debian buster-backports main' > /etc/apt/sources.list.d/buster-backports.list && \ apt update && \ apt -y upgrade && \ - apt -y install libltdl-dev gpg libncurses5 && \ + apt -y install libltdl-dev gpg libncurses5 devscripts && \ apt -y install -t buster-backports apksigner && \ apt-get clean From 4ee2c03b75532fca230981a69ea844f1152c1272 Mon Sep 17 00:00:00 2001 From: Greg Guthe Date: Fri, 1 Oct 2021 09:49:33 -0400 Subject: [PATCH 06/11] signer: gpg2: rename test signer confs --- signer/gpg2/gpg2_test.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/signer/gpg2/gpg2_test.go b/signer/gpg2/gpg2_test.go index b3fbf916c..addf0c4f0 100644 --- a/signer/gpg2/gpg2_test.go +++ b/signer/gpg2/gpg2_test.go @@ -47,7 +47,7 @@ func TestNewSigner(t *testing.T) { t.Run("invalid type", func(t *testing.T) { t.Parallel() - invalidConf := gpg2signerconf + invalidConf := pgpsubkeyGPG2SignerConf invalidConf.Type = "badType" assertNewSignerWithConfErrs(t, invalidConf) }) @@ -55,7 +55,7 @@ func TestNewSigner(t *testing.T) { t.Run("invalid ID", func(t *testing.T) { t.Parallel() - invalidConf := gpg2signerconf + invalidConf := pgpsubkeyGPG2SignerConf invalidConf.ID = "" assertNewSignerWithConfErrs(t, invalidConf) }) @@ -63,7 +63,7 @@ func TestNewSigner(t *testing.T) { t.Run("invalid PrivateKey", func(t *testing.T) { t.Parallel() - invalidConf := gpg2signerconf + invalidConf := pgpsubkeyGPG2SignerConf invalidConf.PrivateKey = "" assertNewSignerWithConfErrs(t, invalidConf) }) @@ -71,7 +71,7 @@ func TestNewSigner(t *testing.T) { t.Run("invalid PublicKey", func(t *testing.T) { t.Parallel() - invalidConf := gpg2signerconf + invalidConf := pgpsubkeyGPG2SignerConf invalidConf.PublicKey = "" assertNewSignerWithConfErrs(t, invalidConf) }) @@ -79,7 +79,7 @@ func TestNewSigner(t *testing.T) { t.Run("invalid KeyID", func(t *testing.T) { t.Parallel() - invalidConf := gpg2signerconf + invalidConf := pgpsubkeyGPG2SignerConf invalidConf.KeyID = "" assertNewSignerWithConfErrs(t, invalidConf) }) @@ -87,7 +87,7 @@ func TestNewSigner(t *testing.T) { t.Run("non-alphnumeric KeyID", func(t *testing.T) { t.Parallel() - invalidConf := gpg2signerconf + invalidConf := pgpsubkeyGPG2SignerConf invalidConf.KeyID = "!?;\\" assertNewSignerWithConfErrs(t, invalidConf) }) @@ -244,7 +244,7 @@ var randompgpPrivateKey string //go:embed "test/fixtures/randompgp.pub" var randompgpPublicKey string -var randompgpconf = signer.Configuration{ +var randompgpGPG2SignerConf = signer.Configuration{ ID: "gpg2test-randompgp", Type: Type, KeyID: "0xDD0A5D99AAAB1F1A", @@ -259,7 +259,7 @@ var pgpsubkeyPrivateKey string //go:embed "test/fixtures/pgpsubkey.pub" var pgpsubkeyPublicKey string -var gpg2signerconf = signer.Configuration{ +var pgpsubkeyGPG2SignerConf = signer.Configuration{ ID: "gpg2test", Type: Type, KeyID: "0xE09F6B4F9E6FDCCB", @@ -269,6 +269,6 @@ var gpg2signerconf = signer.Configuration{ } var validSignerConfigs = []signer.Configuration{ - randompgpconf, - gpg2signerconf, + randompgpGPG2SignerConf, + randompgpDebsignSignerConf, } From 1a2d26c803942d1e1ef457051916a339af789ba7 Mon Sep 17 00:00:00 2001 From: Greg Guthe Date: Mon, 4 Oct 2021 11:49:48 -0400 Subject: [PATCH 07/11] docker: build autograph-client --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index 33621c595..a051ffe80 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,6 +27,7 @@ ADD version.json /app RUN cd /app/src/autograph && go install . RUN cd /app/src/autograph/tools/autograph-monitor && go build -o /go/bin/autograph-monitor . +RUN cd /app/src/autograph/tools/autograph-client && go build -o /go/bin/autograph-client . USER app WORKDIR /app From 13091367e5f829a04d60664f3a69cfa5e24cdc51 Mon Sep 17 00:00:00 2001 From: Greg Guthe Date: Tue, 28 Sep 2021 16:21:58 -0400 Subject: [PATCH 08/11] signer: gpg2: add debsign mode --- autograph.yaml | 338 ++++++++++++ bin/run_integration_tests.sh | 12 +- docs/endpoints.md | 2 +- handlers_test.go | 14 +- signer/gpg2/README.md | 90 +++- signer/gpg2/gpg2.go | 174 ++++++- signer/gpg2/gpg2_test.go | 284 +++++++++- signer/gpg2/test/fixtures/sphinx_1.7.2-1.dsc | 50 ++ .../fixtures/sphinx_1.7.2-1_amd64.buildinfo | 492 ++++++++++++++++++ .../fixtures/sphinx_1.7.2-1_amd64.changes | 74 +++ tools/autograph-client/build_test_gpg.sh | 55 ++ tools/autograph-client/client.go | 94 +++- .../integration_test_gpg2_signer.sh | 26 + 13 files changed, 1664 insertions(+), 41 deletions(-) create mode 100644 signer/gpg2/test/fixtures/sphinx_1.7.2-1.dsc create mode 100644 signer/gpg2/test/fixtures/sphinx_1.7.2-1_amd64.buildinfo create mode 100644 signer/gpg2/test/fixtures/sphinx_1.7.2-1_amd64.changes create mode 100755 tools/autograph-client/build_test_gpg.sh create mode 100755 tools/autograph-client/integration_test_gpg2_signer.sh diff --git a/autograph.yaml b/autograph.yaml index d92598ebe..f2bf02efe 100755 --- a/autograph.yaml +++ b/autograph.yaml @@ -730,6 +730,7 @@ signers: - id: randompgp type: gpg2 + mode: gpg2 keyid: 0xDD0A5D99AAAB1F1A privatekey: | -----BEGIN PGP PRIVATE KEY BLOCK----- @@ -823,6 +824,102 @@ signers: =3Atg -----END PGP PUBLIC KEY BLOCK----- + - id: randompgp-debsign + type: gpg2 + mode: debsign + keyid: A2910E4FBEA076009BCDE536DD0A5D99AAAB1F1A + privatekey: | + -----BEGIN PGP PRIVATE KEY BLOCK----- + + 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 + oOuLM3lEWUfh + =SRmt + -----END PGP PRIVATE KEY BLOCK----- + publickey: | + -----BEGIN PGP PUBLIC KEY BLOCK----- + + mQENBFuW9xABCACzCLYHwgGba7hi+lwhD/Hr5qqpg+UuN+88NclYgLWyl1nPpx2D + JvH6p7ASj2P9BzEp0XatXLO4/uPQY2UX9UpWLT5wDGOdX4QCvZvFk4whcXHtcamr + IQFTUjxRSIqvrq4t1h/4z635ztN0C6h5fWCxrCsoPJNQwEG/ZSDNXfwrJbsTIgus + X037WXAzCYKzDZg9dGcUon4F2DHGGGqjOqLsyaGvOvOPddhorESuAJRe6Tl9ijzT + NGc1uXIVEjEa5v9L4DJDqXYJqG35e0UuLkg0Wz4V9RVW/QP5DgnJAMQ8DUkXNHpa + eD1H9Zg/EBt3/85BGCR7u7J6MYvhuVnLIXQ1ABEBAAG0K01vemlsbGEgQXV0b2dy + YXBoIERldiA8bm9yZXBseUBleGFtcGxlLm5ldD6JAVQEEwEKAD4CGwMFCwkIBwMF + FQoJCAsFFgIDAQACHgECF4AWIQSikQ5PvqB2AJvN5TbdCl2ZqqsfGgUCYGs5MgUJ + KmxIIgAKCRDdCl2ZqqsfGuuYB/0b95Wk7JS6hy6d3HGQfiK0V1aFjTJydaCoEnB2 + /sT3Z4TvW+mUuAOVkGrpTi01s+difWpDcIE+mxKEOc7t2GB36U7uWV+7aNUMWPLo + fhEAMQLVojonzKtR//9Iy9wqshMNEZ7ed7PA0Ju9DGpQJpUUWahmYHVbVpRtxJ92 + M5CJvHNy0HYx8JIM89en4r87kPi/cMsPHo7z1sHvv4MbRw7POpVIO5uYefxd5N4N + HuenYmsfb4KhmVgMjiI6td5xF17tWLXjG3rcKKWRxmPhyKI1XLR5RRer0jLrd4Ge + rdGc96yQEPkQ6Th23V4imOSJBpJ/msSSBFWv5bb3mJdzabDvuQENBFuW9xABCADI + ykOe+xwyVVBNQsy+Bfzk5JsbjWmLcb/7sAy82TVSpq5LMm9v0OFlKMzpD6EjPGe8 + wrk1OFHGXkLFhOvprpiZYxOnLbtcLbJ0mkgU3azdLsvBEFrDLv0N8AEdWBptkMCc + ar53W3iLKqxi9eKJCE86K6T7BtCR/NQ+SAUSz4Cv1mZacxMv8FZ5IllvsNmIFyoy + 4mQx+tVS5OzNsd1D8gk93NlHQs82od4HI7BxUTnJgB0oZZz58MHCjjHIHCBp71RN + RFRufFArnrHsFkVxHFJH00Yn3WnJi4G5/IuZ8jUmBNFZ7JknwaHn5DX3XbF996MI + YVZtZtnQk0LdMQqVNs/rABEBAAGJATwEGAEKACYCGwwWIQSikQ5PvqB2AJvN5Tbd + Cl2ZqqsfGgUCYGs5DwUJKmxH/wAKCRDdCl2ZqqsfGiC5CAClOM2l9IRIl/iDCn+R + lMW1B12YP4/pPfco0KUMJwlb/lk0uybYjn3o9FbMS7qDlHQ8fn8SreaE4mdJA9Lq + hOahIUeDjfcem5/rPEWNfoiIQELsyvX2DZzjthHwh6+BCBKIekiBRDWj0AYeLtmv + M1vx6uxbvvIZsdOy7zU+jBFIGmL1GOwaaLuTv24EUwGCDq04j/fa5LaVuOSyCXnt + ask+6gjl1qr6zI44+IgZBySoJL86UxPUv4QVXCqgsvKoqMXggPpGEwGIIQec8mGC + Pl9w2LbeqzvlZonAvK/iLPHtaVcMg6fDa/OgKrYcGdfRIhX3u8uiyjLAoOuLM3lE + WUfh + =3Atg + -----END PGP PUBLIC KEY BLOCK----- + - id: pgpsubkey # TEST 4096-bit RSA public / dev / signing subkey without master type: gpg2 @@ -1061,6 +1158,245 @@ signers: =459B -----END PGP PUBLIC KEY BLOCK----- + - id: pgpsubkey-debsign + # TEST 4096-bit RSA public / dev / signing subkey without master + type: gpg2 + mode: debsign + keyid: 1D02D42C7C2086373E2B7D8ED01EF1FA33C6BAEB + passphrase: abcdef123 + privatekey: | + -----BEGIN PGP PRIVATE KEY BLOCK----- + + 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 + bWI= + =J9W0 + -----END PGP PRIVATE KEY BLOCK----- + publickey: | + -----BEGIN PGP PUBLIC KEY BLOCK----- + + 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 + gAAPg+05bWI= + =459B + -----END PGP PUBLIC KEY BLOCK----- + - id: dummyrsapss # a test 2048-bit RSA key type: genericrsa @@ -1340,7 +1676,9 @@ authorizations: - testmar - testmarecdsa - randompgp + - randompgp-debsign - pgpsubkey + - pgpsubkey-debsign - dummyrsapss - dummyrsa - testauthenticode diff --git a/bin/run_integration_tests.sh b/bin/run_integration_tests.sh index 223f9fe23..958acf4a8 100755 --- a/bin/run_integration_tests.sh +++ b/bin/run_integration_tests.sh @@ -68,6 +68,16 @@ docker-compose run \ --entrypoint ./integration_test_api.sh \ app-hsm +echo "checking gpg signing" +docker-compose run \ + --rm \ + --user 0 \ + -e AUTOGRAPH_URL=http://app:8000 \ + --workdir /app/src/autograph/tools/autograph-client \ + --entrypoint ./integration_test_gpg2_signer.sh \ + app +# TODO(GH-785): add HSM support for GPG signing keys and test here + echo "checking XPI signing" docker-compose run \ --rm \ @@ -94,4 +104,4 @@ docker-compose run \ --workdir /app/src/autograph/tools/autograph-client \ --entrypoint ./build_test_apks.sh \ app -# TODO: add HSM support for APK signing keys and test here +# TODO(GH-381): add HSM support for APK signing keys and test here diff --git a/docs/endpoints.md b/docs/endpoints.md index f126138fb..7dbedd473 100644 --- a/docs/endpoints.md +++ b/docs/endpoints.md @@ -121,7 +121,7 @@ cannot occur next to each other in the file name. example: ``` bash -POST /sign/file +POST /sign/files Host: autograph.example.net Content-type: application/json Authorization: Hawk id="alice", mac="756lSgQEYLoc6V0Uv2wS8pRg/h+4WFUVKWQynCFvY8Y=", ts="1524487134", nonce="MrpGL35q", hash="9m3WhtGQDuHermi5fDYBGJlOqNeK5B3nk0lKreZ+YSw=", ext="933126753" diff --git a/handlers_test.go b/handlers_test.go index 4ce61b0b2..9e13eede2 100644 --- a/handlers_test.go +++ b/handlers_test.go @@ -94,6 +94,14 @@ func TestBadRequest(t *testing.T) { {`/sign/files`, `POST`, `[{"files": [{"name": "spam/eggs/foo.dsc", "content":"aGVsbG8="}], "keyid": "randompgp-debsign"}]`}, // file name is windows path {`/sign/files`, `POST`, `[{"files": [{"name": "C:\spam\eggs\foo.dsc", "content":"aGVsbG8="}], "keyid": "randompgp-debsign"}]`}, + // file name is file:// url + {`/sign/files`, `POST`, `[{"files": [{"name": "file:///etc/hosts", "content":"aGVsbG8="}], "keyid": "randompgp-debsign"}]`}, + // file name with @ + {`/sign/files`, `POST`, `[{"files": [{"name": "file@localhost.wtf", "content":"aGVsbG8="}], "keyid": "randompgp-debsign"}]`}, + // file name beginning with \0 + {`/sign/files`, `POST`, `[{"files": [{"name": "\0file", "content":"aGVsbG8="}], "keyid": "randompgp-debsign"}]`}, + // file name containing \0 + {`/sign/files`, `POST`, `[{"files": [{"name": "\0/file", "content":"aGVsbG8="}], "keyid": "randompgp-debsign"}]`}, // file name is >255 chars (404 long) {`/sign/files`, `POST`, `[{"files": [{"name": "spamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspamspam.dsc", "content":"aGVsbG8="}], "keyid": "randompgp-debsign"}]`}, @@ -566,6 +574,8 @@ func TestDebug(t *testing.T) { func TestHandleGetAuthKeyIDs(t *testing.T) { t.Parallel() + const autographDevAliceKeyIDsJSON = "[\"apk_cert_with_ecdsa_sha256\",\"apk_cert_with_ecdsa_sha256_v3\",\"appkey1\",\"appkey2\",\"dummyrsa\",\"dummyrsapss\",\"extensions-ecdsa\",\"extensions-ecdsa-expired-chain\",\"legacy_apk_with_rsa\",\"normandy\",\"pgpsubkey\",\"pgpsubkey-debsign\",\"randompgp\",\"randompgp-debsign\",\"remote-settings\",\"testapp-android\",\"testapp-android-legacy\",\"testapp-android-v3\",\"testauthenticode\",\"testmar\",\"testmarecdsa\",\"webextensions-rsa\",\"webextensions-rsa-with-recommendation\"]" + var testcases = []struct { name string method string @@ -633,7 +643,7 @@ func TestHandleGetAuthKeyIDs(t *testing.T) { body: "", authorizeID: "alice", expectedStatus: http.StatusOK, - expectedBody: "[\"apk_cert_with_ecdsa_sha256\",\"apk_cert_with_ecdsa_sha256_v3\",\"appkey1\",\"appkey2\",\"dummyrsa\",\"dummyrsapss\",\"extensions-ecdsa\",\"extensions-ecdsa-expired-chain\",\"legacy_apk_with_rsa\",\"normandy\",\"pgpsubkey\",\"randompgp\",\"remote-settings\",\"testapp-android\",\"testapp-android-legacy\",\"testapp-android-v3\",\"testauthenticode\",\"testmar\",\"testmarecdsa\",\"webextensions-rsa\",\"webextensions-rsa-with-recommendation\"]", + expectedBody: autographDevAliceKeyIDsJSON, expectedHeaders: http.Header{"Content-Type": []string{"application/json"}}, }, { @@ -707,7 +717,7 @@ func TestHandleGetAuthKeyIDs(t *testing.T) { nilBody: true, authorizeID: "alice", expectedStatus: http.StatusOK, - expectedBody: "[\"apk_cert_with_ecdsa_sha256\",\"apk_cert_with_ecdsa_sha256_v3\",\"appkey1\",\"appkey2\",\"dummyrsa\",\"dummyrsapss\",\"extensions-ecdsa\",\"extensions-ecdsa-expired-chain\",\"legacy_apk_with_rsa\",\"normandy\",\"pgpsubkey\",\"randompgp\",\"remote-settings\",\"testapp-android\",\"testapp-android-legacy\",\"testapp-android-v3\",\"testauthenticode\",\"testmar\",\"testmarecdsa\",\"webextensions-rsa\",\"webextensions-rsa-with-recommendation\"]", + expectedBody: autographDevAliceKeyIDsJSON, expectedHeaders: http.Header{"Content-Type": []string{"application/json"}}, }, } diff --git a/signer/gpg2/README.md b/signer/gpg2/README.md index 8216ba938..f47facd8c 100644 --- a/signer/gpg2/README.md +++ b/signer/gpg2/README.md @@ -1,19 +1,13 @@ # PGP Signing with GPG2 -This signer implements the Pretty Good Privacy signature format. It -accepts data on the `/sign/data` interface and returns -armored detached signatures like the `pgp` signer. +This signer implements the Pretty Good Privacy signature format and +Debian GPG signing using `debsign`. -**Try the \`pgp\` signer first since it keeps private keys in memory, -which is more secure.** +When configured in `gpg2` mode it accepts data on the `/sign/data` +interface and returns armored detached signatures. -Only use the this signer if the `pgp` signer doesn\'t -understand your key format and you need to load private keys with -passphrases exported with the [unsupported and non-standard gnu-dummy -S2K algorithm](https://github.com/golang/go/issues/13605) and sign with -a subkey. Also prefer the `pgp` signer, since this signer 1) -requires a the gpg2 binary with version \>2.1, 2) writes private keys to -keyrings on disk, and 3) shells out to the `gpg2` binary. +In `debsign` mode it accepts files on the `/sign/files` interface and +returns the clearsigned files. Example Usage: @@ -69,9 +63,34 @@ signers: -----END PGP PUBLIC KEY BLOCK----- ``` +The **optional** field `mode` it can be either `gpg2` or +`debsign`. When empty or missing it defaults to `gpg2` and should use +the full key fingerprint in the `keyid` field. For example: + +```yaml +- id: some-pgp-key + type: gpg2 + mode: debsign + keyid: A2910E4FBEA076009BCDE536DD0A5D99AAAB1F1A + passphrase: abcdef123 + privatekey: | + -----BEGIN PGP PRIVATE KEY BLOCK----- + + lQOYBFuW9xABCACzCLYHwgGba7hi+lwhD/Hr5qqpg+UuN+88NclYgLWyl1nPpx2D + ... + HQASoA7mirON + =vJUu + -----END PGP PRIVATE KEY BLOCK----- + publickey: | + -----BEGIN PGP PUBLIC KEY BLOCK----- + ... + =459B + -----END PGP PUBLIC KEY BLOCK----- +``` + ## Signature request -This signer only supports the `/sign/data/` endpoint. +This signer only supports the `/sign/data/` endpoint in `gpg2` mode: ``` json [ @@ -82,6 +101,27 @@ This signer only supports the `/sign/data/` endpoint. ] ``` +This signer only supports the `/sign/files/` endpoint in `debsign` mode: + +``` json +[ + { + "input": "", + "keyid": "pgpsubkey-debsign", + "signed_files": [ + { + "name": "sphinx_1.7.2-1.dsc", + "content": "LS0tLS1CRUdJTiBQR1AgU0lHTkVEIE1FU1NBR0UtLS0tLQpIYXNoOiBTS..." + }, + { + "name": "sphinx_1.7.2-1_amd64.buildinfo", + "content": "LS0tLS1CRUdJTiBQR1AgU0lHTkVEIE1FU1NBR0UtLS0tLQpIYXNoOiBTS..." + } + ] + } +] +``` + ## Signature response The response to a data signing request contains a PGP armored detached @@ -100,3 +140,27 @@ recover the standard armored signature that gnupg expects. } ] ``` + +In `debsign` mode responses are at the field `signed_files`: + +```json +[ + { + "ref": "boxfa5qavzf11p6zme2pd74tn", + "type": "gpg2", + "mode": "debsign", + "signer_id": "pgpsubkey-debsign", + "public_key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBFwaoDMBEAC0FVHFLTVYFSr8ZpCWOKyF+Xrpcr032pOr3p3rBH6Ld9ZTpaLS...", + "signed_files": [ + { + "name": "sphinx_1.7.2-1.dsc", + "content": "LS0tLS1CRUdJTiBQR1AgU0lHTkVEIE1FU1NBR0UtLS0tLQpIYXNoOiBTS..." + }, + { + "name": "sphinx_1.7.2-1_amd64.buildinfo", + "content": "LS0tLS1CRUdJTiBQR1AgU0lHTkVEIE1FU1NBR0UtLS0tLQpIYXNoOiBTS..." + } + ] + } +] +``` diff --git a/signer/gpg2/gpg2.go b/signer/gpg2/gpg2.go index f230e4ef3..73f943e39 100644 --- a/signer/gpg2/gpg2.go +++ b/signer/gpg2/gpg2.go @@ -1,6 +1,7 @@ package gpg2 import ( + "bytes" "fmt" "io" "io/ioutil" @@ -8,6 +9,7 @@ import ( "os/exec" "path/filepath" "regexp" + "strings" "sync" "github.com/mozilla-services/autograph/signer" @@ -24,10 +26,40 @@ const ( // passphrases https://github.com/golang/go/issues/13605 Type = "gpg2" + // ModeGPG2 represents a signer that signs data with gpg2 + ModeGPG2 = "gpg2" + + // ModeDebsign represents a signer that signs files with debsign + ModeDebsign = "debsign" + keyRingFilename = "autograph_gpg2_keyring.gpg" secRingFilename = "autograph_gpg2_secring.gpg" + gpgConfFilename = "gpg.conf" + + // gpgConfContentsHead is the static part of the gpg config + // + // options from https://www.gnupg.org/documentation/manuals/gnupg/GPG-Configuration-Options.html + // + // batch: Use batch mode. Never ask, do not allow interactive commands... + // no-tty: Make sure that the TTY (terminal) is never used for any output. This option is needed in some cases because GnuPG sometimes prints warnings to the TTY even if --batch is used. + // yes: Assume "yes" on most questions. Should not be used in an option file. + // + // more options from https://www.gnupg.org/documentation/manuals/gnupg/GPG-Esoteric-Options.html + // + // no-default-keyring: Do not add the default keyrings to the list of keyrings. ... + // passphrase-fd: Read the passphrase from file descriptor n. Only the first line will be read from file descriptor n. If you use 0 for n, the passphrase will be read from STDIN. This can only be used if only one passphrase is supplied. + // pinentry-mode: Set the pinentry mode to mode. Allowed values for mode are: + // ... + // loopback - Redirect Pinentry queries to the caller. Note that in contrast to Pinentry the user is not prompted again if he enters a bad password. + gpgConfContentsHead = `batch +no-default-keyring +no-tty +passphrase-fd 0 +pinentry-mode loopback +yes` ) +var monitoringInputData = []byte(`AUTOGRAPH MONITORING`) var isAlphanumeric = regexp.MustCompile(`^[a-zA-Z0-9]+$`).MatchString // gpg2 fails when multiple signers are called at in parallel so we serialize @@ -50,6 +82,9 @@ type GPG2Signer struct { // tmpDir is the signer's temporary working directory. It // holds the gpg sec and keyrings tmpDir string + + // Mode is which signing command to use gpg2 or debsign + Mode string } // New initializes a pgp signer using a configuration @@ -66,6 +101,16 @@ func New(conf signer.Configuration) (s *GPG2Signer, err error) { } s.ID = conf.ID + switch conf.Mode { + case ModeDebsign: + case ModeGPG2: + case "": // default to signing in gpg2 mode to preserve backwards compat with old config files + conf.Mode = ModeGPG2 + default: + return nil, fmt.Errorf("gpg2: unknown signer mode %q, must be 'gpg2', or 'debsign'", conf.Mode) + } + s.Mode = conf.Mode + if conf.PrivateKey == "" { return nil, fmt.Errorf("gpg2: missing private key in signer configuration") } @@ -92,6 +137,15 @@ func New(conf signer.Configuration) (s *GPG2Signer, err error) { if err != nil { return nil, fmt.Errorf("gpg2: error creating keyring: %w", err) } + + // debsign lets us to specify a gpg program name (gpg or + // gpg2), but not args. We use a config file to sent them. + if s.Mode == ModeDebsign { + // write gpg.conf after importing keys, so gpg doesn't try to read stdin for key imports + if err = writeGPGConf(s.tmpDir); err != nil { + return nil, fmt.Errorf("error writing gpg conf: %w", err) + } + } return } @@ -100,7 +154,7 @@ func New(conf signer.Configuration) (s *GPG2Signer, err error) { // director holding the rings func createKeyRing(s *GPG2Signer) (string, error) { // reuse keyring in tempdir - prefix := fmt.Sprintf("autograph_%s_%s", s.Type, s.KeyID) + prefix := fmt.Sprintf("autograph_%s_%s_%s_", s.Type, s.KeyID, s.Mode) dir, err := ioutil.TempDir("", prefix) if err != nil { @@ -134,6 +188,9 @@ func createKeyRing(s *GPG2Signer) (string, error) { // call gpg to create a new keyring and load the public key in it gpgLoadPublicKey := exec.Command("gpg", + // Shortcut for --options /dev/null. This option is detected before an attempt to open an option file. Using this option will also prevent the creation of a ~/.gnupg homedir. + "--no-options", + "--homedir", dir, "--no-default-keyring", "--keyring", keyRingPath, "--secret-keyring", secRingPath, @@ -142,28 +199,48 @@ func createKeyRing(s *GPG2Signer) (string, error) { "--yes", "--import", tmpPublicKeyFile.Name(), ) + gpgLoadPublicKey.Dir = dir out, err := gpgLoadPublicKey.CombinedOutput() if err != nil { return "", fmt.Errorf("gpg2: failed to load public key into keyring: %s\n%s", err, out) } - log.Debugf(fmt.Sprintf("gpg2: loaded public key %s", string(out))) + log.Debugf("gpg2: loaded public key %s", string(out)) // call gpg to load the private key in it gpgLoadPrivateKey := exec.Command("gpg", "--no-default-keyring", + // Shortcut for --options /dev/null. This option is detected before an attempt to open an option file. Using this option will also prevent the creation of a ~/.gnupg homedir. + "--no-options", + "--homedir", dir, "--keyring", keyRingPath, "--secret-keyring", secRingPath, "--no-tty", "--batch", "--yes", "--import", tmpPrivateKeyFile.Name()) + gpgLoadPrivateKey.Dir = dir out, err = gpgLoadPrivateKey.CombinedOutput() if err != nil { return "", fmt.Errorf("gpg2: failed to load private key into keyring: %s\n%s", err, out) } - log.Debugf(fmt.Sprintf("gpg2: loaded private key %s", string(out))) + log.Debugf("gpg2: loaded private key %s", string(out)) return dir, nil +} + +// writeGPGConf writes a GPG config files in gpgHomeDir. It appends +// keyring and homedir options. +func writeGPGConf(gpgHomeDir string) error { + keyRingPath := filepath.Join(gpgHomeDir, keyRingFilename) + gpgConfPath := filepath.Join(gpgHomeDir, gpgConfFilename) + gpgConfContents := fmt.Sprintf("%s\nkeyring %s\nhomedir %s\n", gpgConfContentsHead, keyRingPath, gpgHomeDir) + + err := ioutil.WriteFile(gpgConfPath, []byte(gpgConfContents), 0600) + if err != nil { + return err + } + log.Debugf("gpg2: wrote config to %s with contents: %s", gpgConfPath, gpgConfContents) + return nil } // AtExit removes the temp dir containing the signer key and sec rings @@ -183,11 +260,15 @@ func (s *GPG2Signer) Config() signer.Configuration { Type: s.Type, PrivateKey: s.PrivateKey, PublicKey: s.PublicKey, + Mode: s.Mode, } } // SignData takes data and returns an armored signature with pgp header and footer func (s *GPG2Signer) SignData(data []byte, options interface{}) (signer.Signature, error) { + if s.Mode != ModeGPG2 && !bytes.Equal(data, monitoringInputData) { + return nil, fmt.Errorf("gpg2: can only sign monitor data in %s mode", ModeGPG2) + } keyRingPath := filepath.Join(s.tmpDir, keyRingFilename) secRingPath := filepath.Join(s.tmpDir, secRingFilename) @@ -207,6 +288,9 @@ func (s *GPG2Signer) SignData(data []byte, options interface{}) (signer.Signatur defer serializeSigning.Unlock() gpgDetachSign := exec.Command("gpg", + // Shortcut for --options /dev/null. This option is detected before an attempt to open an option file. Using this option will also prevent the creation of a ~/.gnupg homedir. + "--no-options", + "--homedir", s.tmpDir, "--no-default-keyring", "--keyring", keyRingPath, "--secret-keyring", secRingPath, @@ -220,6 +304,7 @@ func (s *GPG2Signer) SignData(data []byte, options interface{}) (signer.Signatur "--passphrase-fd", "0", "--detach-sign", tmpContentFile.Name(), ) + gpgDetachSign.Dir = s.tmpDir stdin, err := gpgDetachSign.StdinPipe() if err != nil { return nil, fmt.Errorf("gpg2: failed to create stdin pipe for sign cmd: %w", err) @@ -269,3 +354,86 @@ type Options struct { func (s *GPG2Signer) GetDefaultOptions() interface{} { return Options{} } + +// SignFiles uses debsign to gpg2 clearsign multiple named +// *.buildinfo, *.dsc, or *.changes files +func (s *GPG2Signer) SignFiles(inputs []signer.NamedUnsignedFile, options interface{}) (signedFiles []signer.NamedSignedFile, err error) { + if s.Mode != ModeDebsign { + err = fmt.Errorf("gpg2: can only sign multiple files in %s mode", ModeDebsign) + return + } + + // create a tmp dir outside the signer GPG home + inputsTmpDir, err := ioutil.TempDir("", fmt.Sprintf("autograph_%s_%s_%s_sign_files", s.Type, s.KeyID, s.Mode)) + if err != nil { + err = fmt.Errorf("gpg2: error creating tempdir for debsign: %w", err) + return + } + defer os.RemoveAll(inputsTmpDir) + + // write the inputs to their tmp dir + var inputFilePaths []string + for i, input := range inputs { + ext := filepath.Ext(input.Name) + if !(ext == ".buildinfo" || ext == ".dsc" || ext == ".changes") { + return nil, fmt.Errorf("gpg2: cannot sign file %d. Files missing extension .buildinfo, .dsc, or .changes", i) + } + inputFilePath := filepath.Join(inputsTmpDir, input.Name) + err := ioutil.WriteFile(inputFilePath, input.Bytes, 0644) + if err != nil { + return nil, fmt.Errorf("gpg2: failed to write tempfile %d for debsign to sign: %w", i, err) + } + inputFilePaths = append(inputFilePaths, inputFilePath) + } + + // take a mutex to prevent multiple invocations of gpg in parallel + serializeSigning.Lock() + defer serializeSigning.Unlock() + + args := append([]string{ + // "Do not read any configuration files. This can only be used as the first option given on the command-line." + "--no-conf", + // "Specify the key ID to be used for signing; overrides any -m and -e options." + // debsign prefers the pub key fingerprint: https://github.com/Debian/devscripts/blob/16f9a6d24f4bd564c315f81b89e08c3b4fb76f13/scripts/debsign.sh#L389 + "-k", s.KeyID, + // "Recreate signature" + "--re-sign", + }, inputFilePaths...) + debsignCmd := exec.Command("debsign", args...) + debsignCmd.Env = append(os.Environ(), + fmt.Sprintf("GNUPGHOME=%s", s.tmpDir), + ) + stdin, err := debsignCmd.StdinPipe() + if err != nil { + return nil, fmt.Errorf("gpg2: failed to create stdin pipe for debsign cmd: %w", err) + } + + // write passphrase multiple times to stdin + // our gpg.conf prompts for the passphrase on each gpg call + // and debsign can call gpg multiple times per file + passphrasesForStdin := strings.Repeat(fmt.Sprintf("%s\n", s.passphrase), len(inputFilePaths)*4) + if _, err = io.WriteString(stdin, passphrasesForStdin); err != nil { + return nil, fmt.Errorf("gpg2: failed to write passphrase to stdin pipe for debsign cmd: %w", err) + } + if err = stdin.Close(); err != nil { + return nil, fmt.Errorf("gpg2: failed to close to stdin pipe for debsign cmd: %w", err) + } + out, err := debsignCmd.CombinedOutput() + if err != nil { + return nil, fmt.Errorf("gpg2: failed to debsign inputs %s\n%s", err, out) + } + + // read the signed tempfiles + for i, inputFilePath := range inputFilePaths { + signedFileBytes, err := ioutil.ReadFile(inputFilePath) + if err != nil { + return nil, fmt.Errorf("gpg2: failed to read %d %q signed by debsign: %w", i, inputFilePath, err) + } + signedFiles = append(signedFiles, signer.NamedSignedFile{ + Name: inputs[i].Name, + Bytes: signedFileBytes, + }) + } + log.Debugf("debsign output:\n%s\n", string(out)) + return signedFiles, nil +} diff --git a/signer/gpg2/gpg2_test.go b/signer/gpg2/gpg2_test.go index addf0c4f0..2a489a9f0 100644 --- a/signer/gpg2/gpg2_test.go +++ b/signer/gpg2/gpg2_test.go @@ -7,6 +7,8 @@ import ( "io/ioutil" "os" "os/exec" + "path/filepath" + "strings" "testing" "github.com/mozilla-services/autograph/signer" @@ -15,7 +17,7 @@ import ( func assertNewSignerWithConfOK(t *testing.T, conf signer.Configuration) *GPG2Signer { s, err := New(conf) if s == nil { - t.Fatal("expected non-nil signer for valid conf, but got nil signer") + t.Fatal("expected non-nil signer for valid conf, but got nil signer and err %w", err) } if err != nil { t.Fatalf("signer initialization failed with: %v", err) @@ -33,6 +35,64 @@ func assertNewSignerWithConfErrs(t *testing.T, invalidConf signer.Configuration) } } +// assertClearSignedFilesVerify creates a temp directory +// writes and imports the signer's public key in a new GPG keyring +// then writes and verifies each clear signed file +func assertClearSignedFilesVerify(t *testing.T, signer *GPG2Signer, testname string, signedFiles []signer.NamedSignedFile) { + tmpDir, err := ioutil.TempDir("", fmt.Sprintf("autograph_gpg2_test_%s_", testname)) + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + // write the public key to a file + publicKeyPath := filepath.Join(tmpDir, "gpg2_publickey") + err = ioutil.WriteFile(publicKeyPath, []byte(signer.PublicKey), 0755) + if err != nil { + t.Fatal(err) + } + // call gnupg to create a new keyring, load the key in it + // t.Logf("loading public key %s\n", signer.PublicKey) + gnupgCreateKeyring := exec.Command("gpg", + "--no-options", + "--homedir", tmpDir, + "--no-default-keyring", + "--keyring", filepath.Join(tmpDir, "autograph_test_gpg2_keyring.gpg"), + "--secret-keyring", filepath.Join(tmpDir, "autograph_test_gpg2_secring.gpg"), + "--import", publicKeyPath) + out, err := gnupgCreateKeyring.CombinedOutput() + if err != nil { + t.Fatalf("failed to load public key into keyring: %s\n%s", err, out) + } + // t.Logf("load pubkey out:\n%s", out) + + // write and verify each clear signed file + // gpg --verify considers more than one signed file a detached sig + for _, signedFile := range signedFiles { + signedFilePath := filepath.Join(tmpDir, signedFile.Name) + err = ioutil.WriteFile(signedFilePath, signedFile.Bytes, 0755) + if err != nil { + t.Fatal(err) + } + // verify the signature + gnupgVerifySig := exec.Command("gpg", + "--no-options", + "--homedir", tmpDir, + "--no-default-keyring", + "--keyring", filepath.Join(tmpDir, "autograph_test_gpg2_keyring.gpg"), + "--secret-keyring", filepath.Join(tmpDir, "autograph_test_gpg2_secring.gpg"), + "--batch", + "--yes", + "--pinentry-mode", "error", + "--verify", signedFilePath) + out, err = gnupgVerifySig.CombinedOutput() + if err != nil { + t.Fatalf("error verifying detached sig: %s\n%s", err, out) + } + t.Logf("GnuPG PGP signature verification output:\n%s\n", out) + } +} + func TestNewSigner(t *testing.T) { t.Parallel() @@ -44,6 +104,17 @@ func TestNewSigner(t *testing.T) { }) } + t.Run("signer with empty mode defaults to gpg2 mode", func(t *testing.T) { + t.Parallel() + + conf := pgpsubkeyGPG2SignerConf + conf.Mode = "" + s := assertNewSignerWithConfOK(t, conf) + if s.Mode != ModeGPG2 { + t.Fatal("gpg signer with empty str for mode did not default to gpg2 mode") + } + }) + t.Run("invalid type", func(t *testing.T) { t.Parallel() @@ -60,6 +131,14 @@ func TestNewSigner(t *testing.T) { assertNewSignerWithConfErrs(t, invalidConf) }) + t.Run("invalid mode", func(t *testing.T) { + t.Parallel() + + invalidConf := pgpsubkeyGPG2SignerConf + invalidConf.Mode = "system-addon" + assertNewSignerWithConfErrs(t, invalidConf) + }) + t.Run("invalid PrivateKey", func(t *testing.T) { t.Parallel() @@ -134,7 +213,6 @@ func TestOptionsAreEmpty(t *testing.T) { } func TestSignData(t *testing.T) { - for _, conf := range validSignerConfigs { input := []byte("foobarbaz1234abcd") t.Run(fmt.Sprintf("signer %s signs data", conf.ID), func(t *testing.T) { @@ -143,6 +221,12 @@ func TestSignData(t *testing.T) { // sign input data sig, err := s.SignData(input, s.GetDefaultOptions()) + if s.Mode != ModeGPG2 { + if err == nil { + t.Fatalf("signer in mode %s signed GPG2 data unexpectedly", s.Mode) + } + return + } if err != nil { t.Fatalf("failed to sign data: %v", err) } @@ -231,7 +315,153 @@ func TestSignData(t *testing.T) { } t.Logf("GnuPG PGP signature verification output:\n%s\n", out) }) + }) + } +} + +func TestGPG2Signer_SignFiles(t *testing.T) { + type fields struct { + Configuration signer.Configuration + } + type args struct { + inputs []signer.NamedUnsignedFile + options interface{} + } + tests := []struct { + name string + fields fields + args args + wantErr bool + wantErrStr string + wantErrPrefix string + }{ + { + name: fmt.Sprintf("signer %s in mode %s errors", randompgpGPG2SignerConf.ID, randompgpGPG2SignerConf.Mode), + fields: fields{ + Configuration: randompgpGPG2SignerConf, + }, + wantErr: true, + wantErrStr: "gpg2: can only sign multiple files in debsign mode", + }, + { + name: "errors for invalid file extensions", + fields: fields{ + Configuration: pgpsubkeyDebsignSignerConf, + }, + args: args{ + inputs: []signer.NamedUnsignedFile{ + { + Name: "foo.changes", + Bytes: []byte(""), + }, + { + Name: "bar.exe", + Bytes: []byte(""), + }, + }, + options: nil, + }, + wantErr: true, + wantErrStr: "gpg2: cannot sign file 1. Files missing extension .buildinfo, .dsc, or .changes", + }, + { + name: "errors for unsupported .commands file", + fields: fields{ + Configuration: pgpsubkeyDebsignSignerConf, + }, + args: args{ + inputs: []signer.NamedUnsignedFile{ + { + Name: "foo.commands", + Bytes: []byte("invalid"), + }, + }, + options: nil, + }, + wantErr: true, + wantErrStr: "gpg2: cannot sign file 0. Files missing extension .buildinfo, .dsc, or .changes", + }, + { + name: "errors for debsign error on invalid .changes file", + fields: fields{ + Configuration: pgpsubkeyDebsignSignerConf, + }, + args: args{ + inputs: []signer.NamedUnsignedFile{ + { + Name: "foo_bar_amd64.changes", + Bytes: []byte("Files:\ndb1177999615f0aaeed19bf8fc850fc9 3754 python optional sphinx_1.7.2-1.dsc"), + }, + }, + options: nil, + }, + wantErr: true, + wantErrPrefix: "gpg2: failed to debsign inputs exit status 1\ndebsign: Can't find or can't read dsc file", + }, + { + name: "empty files ok", + fields: fields{ + Configuration: pgpsubkeyDebsignSignerConf, + }, + args: args{ + inputs: []signer.NamedUnsignedFile{}, + options: nil, + }, + wantErr: false, + }, + { + name: fmt.Sprintf("signer %s in mode %s ok", randompgpDebsignSignerConf.ID, randompgpDebsignSignerConf.Mode), + fields: fields{ + Configuration: randompgpDebsignSignerConf, + }, + args: args{ + inputs: sphinxDebsignInputs, + options: nil, + }, + wantErr: false, + }, + { + name: fmt.Sprintf("signer %s in mode %s ok", pgpsubkeyDebsignSignerConf.ID, pgpsubkeyDebsignSignerConf.Mode), + fields: fields{ + Configuration: pgpsubkeyDebsignSignerConf, + }, + args: args{ + inputs: sphinxDebsignInputs, + options: nil, + }, + wantErr: false, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + // initialize a signer + s := assertNewSignerWithConfOK(t, tt.fields.Configuration) + + gotSignedFiles, err := s.SignFiles(tt.args.inputs, tt.args.options) + // t.Logf("SignFiles err:\n%q", err) + if tt.wantErr { + if err == nil { + t.Errorf("GPG2Signer.SignFiles() error = %v, wantErr %v", err, tt.wantErr) + } else if !(err.Error() == tt.wantErrStr || strings.HasPrefix(err.Error(), tt.wantErrPrefix)) { + t.Errorf("GPG2Signer.SignFiles() error.Error() = %q, wantErrStr %q or prefix %q", err.Error(), tt.wantErrStr, tt.wantErrPrefix) + } + return + } + if len(gotSignedFiles) != len(tt.args.inputs) { + t.Errorf("GPG2Signer.SignFiles() returned %d signed files != %d input files", len(gotSignedFiles), len(tt.args.inputs)) + } + for i, signedFile := range gotSignedFiles { + // t.Logf("%s:\n%s", signedFile.Name, signedFile.Bytes) + if signedFile.Name != tt.args.inputs[i].Name { + t.Errorf("GPG2Signer.SignFiles() file %d: signed file name %q != input file name %q", i, signedFile.Name, tt.args.inputs[i].Name) + } + } + + assertClearSignedFilesVerify(t, s, "verify-debsigned-files", gotSignedFiles) }) } } @@ -247,28 +477,78 @@ var randompgpPublicKey string var randompgpGPG2SignerConf = signer.Configuration{ ID: "gpg2test-randompgp", Type: Type, + Mode: ModeGPG2, KeyID: "0xDD0A5D99AAAB1F1A", Passphrase: "abcdef123", PrivateKey: randompgpPrivateKey, PublicKey: randompgpPublicKey, } +var randompgpDebsignSignerConf = signer.Configuration{ + ID: "gpg2test-randompgp-debsign", + Type: Type, + Mode: ModeDebsign, + KeyID: "A2910E4FBEA076009BCDE536DD0A5D99AAAB1F1A", + Passphrase: "abcdef123", + PrivateKey: randompgpPrivateKey, + PublicKey: randompgpPublicKey, +} + //go:embed "test/fixtures/pgpsubkey.key" var pgpsubkeyPrivateKey string //go:embed "test/fixtures/pgpsubkey.pub" var pgpsubkeyPublicKey string +// debsign test files from https://github.com/Debian/devscripts/tree/37b5cc1e5e47cf5ff472ef1f8847de547731df44/test/debsign + +//go:embed "test/fixtures/sphinx_1.7.2-1.dsc" +var sphinxDsc []byte + +//go:embed "test/fixtures/sphinx_1.7.2-1_amd64.buildinfo" +var sphinxBuildinfo []byte + +//go:embed "test/fixtures/sphinx_1.7.2-1_amd64.changes" +var sphinxChanges []byte + +var sphinxDebsignInputs = []signer.NamedUnsignedFile{ + { + Name: "sphinx_1.7.2-1.dsc", + Bytes: sphinxDsc, + }, + { + Name: "sphinx_1.7.2-1_amd64.buildinfo", + Bytes: sphinxBuildinfo, + }, + { + Name: "sphinx_1.7.2-1_amd64.changes", + Bytes: sphinxChanges, + }, +} + var pgpsubkeyGPG2SignerConf = signer.Configuration{ ID: "gpg2test", Type: Type, + Mode: ModeGPG2, KeyID: "0xE09F6B4F9E6FDCCB", Passphrase: "abcdef123", PrivateKey: pgpsubkeyPrivateKey, PublicKey: pgpsubkeyPublicKey, } +var pgpsubkeyDebsignSignerConf = signer.Configuration{ + ID: "pgpsubkey-debsign", + Type: Type, + Mode: ModeDebsign, + KeyID: "1D02D42C7C2086373E2B7D8ED01EF1FA33C6BAEB", + Passphrase: "abcdef123", + PrivateKey: pgpsubkeyPrivateKey, + PublicKey: pgpsubkeyPublicKey, +} + var validSignerConfigs = []signer.Configuration{ randompgpGPG2SignerConf, randompgpDebsignSignerConf, + pgpsubkeyGPG2SignerConf, + pgpsubkeyDebsignSignerConf, } diff --git a/signer/gpg2/test/fixtures/sphinx_1.7.2-1.dsc b/signer/gpg2/test/fixtures/sphinx_1.7.2-1.dsc new file mode 100644 index 000000000..cdb30b1b8 --- /dev/null +++ b/signer/gpg2/test/fixtures/sphinx_1.7.2-1.dsc @@ -0,0 +1,50 @@ +-----BEGIN PGP SIGNED MESSAGE----- +Hash: SHA512 + +Format: 3.0 (quilt) +Source: sphinx +Binary: python-sphinx, python3-sphinx, sphinx-common, sphinx-doc, libjs-sphinxdoc +Architecture: all +Version: 1.7.2-1 +Maintainer: Debian Python Modules Team +Uploaders: Dmitry Shachnev , Chris Lamb +Homepage: http://sphinx-doc.org/ +Standards-Version: 4.1.4 +Vcs-Browser: https://salsa.debian.org/python-team/modules/sphinx +Vcs-Git: https://salsa.debian.org/python-team/modules/sphinx.git +Testsuite: autopkgtest +Testsuite-Triggers: dvipng, gir1.2-webkit2-4.0, graphviz, imagemagick-6.q16, librsvg2-bin, python-enum34, python-html5lib, python-mock, python-pygments, python-pytest, python-sphinxcontrib.websupport, python-sqlalchemy, python-whoosh, python-xapian, python3-gi, python3-html5lib, python3-mock, python3-pygments, python3-pytest, python3-sphinxcontrib.websupport, python3-sqlalchemy, python3-whoosh, python3-xapian, texinfo, texlive-fonts-recommended, texlive-latex-extra, texlive-luatex, texlive-xetex, xauth, xvfb +Build-Depends: debhelper (>= 11) +Build-Depends-Indep: dh-python, dh-strip-nondeterminism, dpkg-dev (>= 1.17.14), python-all (>= 2.6.6-4~), python3-all (>= 3.3.3-1~), python3-lib2to3, python-six (>= 1.5), python3-six (>= 1.5), python-setuptools (>= 0.6c5-1~), python3-setuptools, python-docutils (>= 0.11), python3-docutils (>= 0.11), python-pygments (>= 2.1.1), python3-pygments (>= 2.1.1), python-jinja2 (>= 2.3), python3-jinja2 (>= 2.3), python-pytest, python3-pytest, python-mock, python3-mock, python-babel (>= 1.3), python3-babel (>= 1.3), python-alabaster (>= 0.7), python3-alabaster (>= 0.7), python-imagesize, python3-imagesize, python-requests (>= 2.4.0), python3-requests (>= 2.4.0), python-html5lib, python3-html5lib, python-enum34, python-typing, python-packaging, python3-packaging, python3-sphinxcontrib.websupport , libjs-jquery (>= 1.4), libjs-underscore, texlive-latex-recommended, texlive-latex-extra, texlive-fonts-recommended, texinfo, texlive-luatex, texlive-xetex, dvipng, graphviz, imagemagick-6.q16, librsvg2-bin, perl +Package-List: + libjs-sphinxdoc deb javascript optional arch=all + python-sphinx deb python optional arch=all + python3-sphinx deb python optional arch=all + sphinx-common deb python optional arch=all + sphinx-doc deb doc optional arch=all profile=!nodoc +Checksums-Sha1: + 1d1fa6954ae216cd44ea52dfc67063f26939c8f5 4719536 sphinx_1.7.2.orig.tar.gz + facfa686a3a0bc98c269e16e66427f96e00889ad 34268 sphinx_1.7.2-1.debian.tar.xz +Checksums-Sha256: + 5a1c9a0fec678c24b9a2f5afba240c04668edb7f45c67ce2ed008996b3f21ae2 4719536 sphinx_1.7.2.orig.tar.gz + a6a825914b19cfdbc22df858b0cecc497765dad2058deae20a88a6a2f9d57d24 34268 sphinx_1.7.2-1.debian.tar.xz +Files: + 21a08e994e6a289ed14eecefde2b4f2f 4719536 sphinx_1.7.2.orig.tar.gz + e147e2afa47e7e58d1288ad8818c3de0 34268 sphinx_1.7.2-1.debian.tar.xz + +-----BEGIN PGP SIGNATURE----- + +iQIzBAEBCgAdFiEEQw+hF5tfsLeq16ge4J9rT55v3MsFAmFV/jwACgkQ4J9rT55v +3Msymg//ZGcEYU5EqCQd8x3+iex/cpZxIBjHiLpoODMXjQgMEfBKAGAGyiypwjl/ +p9Afj3gAX4arJLJO7lWA5reux9t0KE7xq/zMU46zX67+xDD105meceLYXLihm/G7 +VggHympqtz0fS7y3E/4OLZW+ifW+PAPL9WwCXxc3OabUhV96wA6senNFC75xT6hR +42SYLixgHT22o3ATwQYz8EMp6WmjhbsPZKvMRVeqCVgEYGWIQq3qhzeAssLSxKCG +dyy5e77Bz0/R3l0SL9ylocuEwkeyGlleBvviarK+r8MjE0wgKxR67WFFY9iw4Bdf +emeCuutNkuZZ4avYFYgizjDy/r5Oo4CMeRzByrTZf9IlQ7WllEMmGKmfI6JUHA2u +iUoHwta4GbcRheoTVDvI5bUFtaI+mn+1Lc8GfK2sSuXMoA+nTvDJ60awPdi2YVK7 +Fxdwue1fQayU5v7aMXYhM3IQpJFUHqWtIJZpdu2zFLazaaaMjPVqwVQ4RuifXW+f +IFcewmnfPvNlKiihYrHJJJApgK/wXyOUdvSx4QVB6gI3fYBlH3m5fx+iAKnAP5U0 +ny+HVplJcwdtXbydO1HrZKOEvHgiH1VR19PCRPzqr4W6H94nk3b0WRkBLwYLHc+N +gFZfXlsCve6cErlobKrWNzCD8bhaj63CnFJsiKbJOK9ySfCCafI= +=S6ao +-----END PGP SIGNATURE----- diff --git a/signer/gpg2/test/fixtures/sphinx_1.7.2-1_amd64.buildinfo b/signer/gpg2/test/fixtures/sphinx_1.7.2-1_amd64.buildinfo new file mode 100644 index 000000000..115a69662 --- /dev/null +++ b/signer/gpg2/test/fixtures/sphinx_1.7.2-1_amd64.buildinfo @@ -0,0 +1,492 @@ +-----BEGIN PGP SIGNED MESSAGE----- +Hash: SHA512 + +Format: 1.0 +Source: sphinx +Binary: python-sphinx python3-sphinx sphinx-common sphinx-doc libjs-sphinxdoc +Architecture: all source +Version: 1.7.2-1 +Checksums-Md5: + db1177999615f0aaeed19bf8fc850fc9 3754 sphinx_1.7.2-1.dsc + 4a4df0c2ce6f087414a5dc5802885fd2 88720 libjs-sphinxdoc_1.7.2-1_all.deb + 15b065a197e938715bf501b33066adf2 443380 python-sphinx_1.7.2-1_all.deb + 942189b1dd93faa36faa3552a9d40042 441676 python3-sphinx_1.7.2-1_all.deb + f6a5889bd4e807800239721d365a5f62 432892 sphinx-common_1.7.2-1_all.deb + 86497c90b6cda029576933778357004e 1198352 sphinx-doc_1.7.2-1_all.deb +Checksums-Sha1: + 3c7ba10ca6e46b80044fea71f2486d98c4aebbd1 3754 sphinx_1.7.2-1.dsc + 36a42125afd83731e05596d7037314399622e400 88720 libjs-sphinxdoc_1.7.2-1_all.deb + 8c2ecdf4e4ffd1f4880b486e53c99207c4d69dbc 443380 python-sphinx_1.7.2-1_all.deb + e518d97dceb77d827734d48ac02313fc0ed4b08c 441676 python3-sphinx_1.7.2-1_all.deb + 01e65d2e2c45121e06aff6f46427d6a31251e67c 432892 sphinx-common_1.7.2-1_all.deb + 8bec16b4db2ddf6776f7cfdcf04cd07fe32269ed 1198352 sphinx-doc_1.7.2-1_all.deb +Checksums-Sha256: + 01b74eed4619d52a75707961004f738b6985bd993962b948645dcd061235fe66 3754 sphinx_1.7.2-1.dsc + c6267177819e0a27b8f9296b1d5106e253e84cf0722fbdf8d2bff489befb81d8 88720 libjs-sphinxdoc_1.7.2-1_all.deb + c55dc8f59798a835e6f78adfd95d73f9cf3382e440cdf96b2edbdb1990280f23 443380 python-sphinx_1.7.2-1_all.deb + 3f2b279b572d2fc36d9cc84c9fcded242ed930d42350e1d980c4aed45ee4ae2a 441676 python3-sphinx_1.7.2-1_all.deb + ece661af9ca87723169f48e68a45af0b5157da4b724a185b99ef42ed22e5f378 432892 sphinx-common_1.7.2-1_all.deb + 67722aa0934b9ebce8a870a34542104017491766c2515665c07128ad46b4e6b7 1198352 sphinx-doc_1.7.2-1_all.deb +Build-Origin: Debian +Build-Architecture: amd64 +Build-Date: Sat, 14 Apr 2018 10:19:26 +0100 +Installed-Build-Depends: + adduser (= 3.117), + adwaita-icon-theme (= 3.28.0-1), + autoconf (= 2.69-11), + automake (= 1:1.15.1-3.1), + autopoint (= 0.19.8.1-6), + autotools-dev (= 20180224.1), + base-files (= 10.1), + base-passwd (= 3.5.44), + bash (= 4.4.18-2), + binutils (= 2.30-15), + binutils-common (= 2.30-15), + binutils-x86-64-linux-gnu (= 2.30-15), + bsdmainutils (= 11.1.2), + bsdutils (= 1:2.31.1-0.5), + build-essential (= 12.4), + bzip2 (= 1.0.6-8.1), + ca-certificates (= 20180409), + cgmanager (= 0.41-2), + coreutils (= 8.28-1), + cpp (= 4:7.3.0-3), + cpp-7 (= 7.3.0-16), + dash (= 0.5.8-2.10), + dbus (= 1.12.6-2), + dbus-user-session (= 1.12.6-2), + dconf-gsettings-backend (= 0.28.0-2), + dconf-service (= 0.28.0-2), + debconf (= 1.5.66), + debhelper (= 11.2.1), + debianutils (= 4.8.4), + dh-autoreconf (= 17), + dh-python (= 3.20180326), + dh-strip-nondeterminism (= 0.040-1), + diffutils (= 1:3.6-1), + dmsetup (= 2:1.02.145-4.1), + docutils-common (= 0.14+dfsg-3), + dpkg (= 1.19.0.5), + dpkg-dev (= 1.19.0.5), + dvipng (= 1.15-1), + e2fsprogs (= 1.44.1-2), + fdisk (= 2.31.1-0.5), + file (= 1:5.32-2), + findutils (= 4.6.0+git+20171230-2), + fontconfig (= 2.13.0-4), + fontconfig-config (= 2.13.0-4), + fonts-dejavu-core (= 2.37-1), + fonts-lmodern (= 2.004.5-3), + g++ (= 4:7.3.0-3), + g++-7 (= 7.3.0-16), + gcc (= 4:7.3.0-3), + gcc-7 (= 7.3.0-16), + gcc-7-base (= 7.3.0-16), + gcc-8-base (= 8-20180402-1), + gettext (= 0.19.8.1-6), + gettext-base (= 0.19.8.1-6), + ghostscript (= 9.22~dfsg-2), + glib-networking (= 2.56.0-1), + glib-networking-common (= 2.56.0-1), + glib-networking-services (= 2.56.0-1), + graphviz (= 2.40.1-3), + grep (= 3.1-2), + groff-base (= 1.22.3-10), + gsettings-desktop-schemas (= 3.28.0-1), + gtk-update-icon-cache (= 3.22.29-3), + gzip (= 1.6-5+b1), + hicolor-icon-theme (= 0.17-2), + hostname (= 3.20), + imagemagick-6-common (= 8:6.9.9.39+dfsg-1), + imagemagick-6.q16 (= 8:6.9.9.39+dfsg-1), + init-system-helpers (= 1.51), + intltool-debian (= 0.35.0+20060710.4), + libacl1 (= 2.2.52-3+b1), + libann0 (= 1.1.2+doc-7), + libapparmor1 (= 2.12-4), + libarchive-zip-perl (= 1.60-1), + libargon2-0 (= 0~20161029-1.1), + libasan4 (= 7.3.0-16), + libatk-bridge2.0-0 (= 2.26.2-1), + libatk1.0-0 (= 2.28.1-1), + libatk1.0-data (= 2.28.1-1), + libatomic1 (= 8-20180402-1), + libatspi2.0-0 (= 2.28.0-1), + libattr1 (= 1:2.4.47-2+b2), + libaudit-common (= 1:2.8.2-1), + libaudit1 (= 1:2.8.2-1), + libavahi-client3 (= 0.7-3.1), + libavahi-common-data (= 0.7-3.1), + libavahi-common3 (= 0.7-3.1), + libbinutils (= 2.30-15), + libblkid1 (= 2.31.1-0.5), + libbsd0 (= 0.8.7-1), + libbz2-1.0 (= 1.0.6-8.1), + libc-bin (= 2.27-3), + libc-dev-bin (= 2.27-3), + libc6 (= 2.27-3), + libc6-dev (= 2.27-3), + libcairo-gobject2 (= 1.15.10-2), + libcairo2 (= 1.15.10-2), + libcap-ng0 (= 0.7.7-3.1+b1), + libcap2 (= 1:2.25-1.2), + libcc1-0 (= 8-20180402-1), + libcdt5 (= 2.40.1-3), + libcgmanager0 (= 0.41-2), + libcgraph6 (= 2.40.1-3), + libcilkrts5 (= 7.3.0-16), + libcolord2 (= 1.3.3-2), + libcom-err2 (= 1.44.1-2), + libcomerr2 (= 1.44.1-2), + libcroco3 (= 0.6.12-2), + libcryptsetup12 (= 2:2.0.2-1), + libcups2 (= 2.2.7-3), + libcupsimage2 (= 2.2.7-3), + libdatrie1 (= 0.2.10-7), + libdb5.3 (= 5.3.28-13.1+b1), + libdbus-1-3 (= 1.12.6-2), + libdconf1 (= 0.28.0-2), + libdebconfclient0 (= 0.243), + libdevmapper1.02.1 (= 2:1.02.145-4.1), + libdpkg-perl (= 1.19.0.5), + libdrm-common (= 2.4.91-2), + libdrm2 (= 2.4.91-2), + libegl-mesa0 (= 17.3.8-1), + libegl1 (= 1.0.0+git20180308-1), + libepoxy0 (= 1.4.3-1), + libexpat1 (= 2.2.5-3), + libext2fs2 (= 1.44.1-2), + libfdisk1 (= 2.31.1-0.5), + libffi6 (= 3.2.1-8), + libfftw3-double3 (= 3.3.7-1), + libfile-stripnondeterminism-perl (= 0.040-1), + libfontconfig1 (= 2.13.0-4), + libfreetype6 (= 2.8.1-2), + libfribidi0 (= 0.19.7-2), + libgbm1 (= 17.3.8-1), + libgcc-7-dev (= 7.3.0-16), + libgcc1 (= 1:8-20180402-1), + libgcrypt20 (= 1.8.2-2), + libgd3 (= 2.2.5-4), + libgdbm-compat4 (= 1.14.1-6), + libgdbm5 (= 1.14.1-6), + libgdk-pixbuf2.0-0 (= 2.36.11-2), + libgdk-pixbuf2.0-common (= 2.36.11-2), + libglapi-mesa (= 17.3.8-1), + libglib2.0-0 (= 2.56.1-2), + libglvnd0 (= 1.0.0+git20180308-1), + libgmp10 (= 2:6.1.2+dfsg-3), + libgnutls30 (= 3.5.18-1), + libgomp1 (= 8-20180402-1), + libgpg-error0 (= 1.29-2), + libgraphite2-3 (= 1.3.11-2), + libgs9 (= 9.22~dfsg-2), + libgs9-common (= 9.22~dfsg-2), + libgssapi-krb5-2 (= 1.16-2), + libgtk-3-0 (= 3.22.29-3), + libgtk-3-common (= 3.22.29-3), + libgts-0.7-5 (= 0.7.6+darcs121130-4), + libgvc6 (= 2.40.1-3), + libgvpr2 (= 2.40.1-3), + libharfbuzz-icu0 (= 1.7.6-1), + libharfbuzz0b (= 1.7.6-1), + libhogweed4 (= 3.4-1), + libice6 (= 2:1.0.9-2), + libicu57 (= 57.1-9), + libidn11 (= 1.33-2.2), + libidn2-0 (= 2.0.4-1.1), + libijs-0.35 (= 0.35-13), + libip4tc0 (= 1.6.2-1), + libisl19 (= 0.19-1), + libitm1 (= 8-20180402-1), + libjbig0 (= 2.1-3.1+b2), + libjbig2dec0 (= 0.13-6), + libjpeg62-turbo (= 1:1.5.2-2+b1), + libjs-jquery (= 3.2.1-1), + libjs-sphinxdoc (= 1.6.7-2), + libjs-underscore (= 1.8.3~dfsg-1), + libjson-c3 (= 0.12.1-1.3), + libjson-glib-1.0-0 (= 1.4.2-3), + libjson-glib-1.0-common (= 1.4.2-3), + libk5crypto3 (= 1.16-2), + libkeyutils1 (= 1.5.9-9.2), + libkmod2 (= 25-1), + libkpathsea6 (= 2017.20170613.44572-8+b2), + libkrb5-3 (= 1.16-2), + libkrb5support0 (= 1.16-2), + liblab-gamut1 (= 2.40.1-3), + liblcms2-2 (= 2.9-1), + liblqr-1-0 (= 0.4.2-2.1), + liblsan0 (= 8-20180402-1), + libltdl7 (= 2.4.6-2), + liblz4-1 (= 1.8.1.2-1), + liblzma5 (= 5.2.2-1.3), + libmagic-mgc (= 1:5.32-2), + libmagic1 (= 1:5.32-2), + libmagickcore-6.q16-5 (= 8:6.9.9.39+dfsg-1), + libmagickwand-6.q16-5 (= 8:6.9.9.39+dfsg-1), + libmount1 (= 2.31.1-0.5), + libmpc3 (= 1.1.0-1), + libmpdec2 (= 2.4.2-1), + libmpfr6 (= 4.0.1-1), + libmpx2 (= 8-20180402-1), + libncurses5 (= 6.1-1), + libncursesw5 (= 6.1-1), + libnettle6 (= 3.4-1), + libnih-dbus1 (= 1.0.3-10+b1), + libnih1 (= 1.0.3-10+b1), + libnspr4 (= 2:4.19-1), + libnss3 (= 2:3.36.1-1), + libopenjp2-7 (= 2.3.0-1), + libp11-kit0 (= 0.23.10-2), + libpam-modules (= 1.1.8-3.7), + libpam-modules-bin (= 1.1.8-3.7), + libpam-runtime (= 1.1.8-3.7), + libpam-systemd (= 238-4), + libpam0g (= 1.1.8-3.7), + libpango-1.0-0 (= 1.42.1-1), + libpangocairo-1.0-0 (= 1.42.1-1), + libpangoft2-1.0-0 (= 1.42.1-1), + libpaper-utils (= 1.1.24+nmu5), + libpaper1 (= 1.1.24+nmu5), + libpathplan4 (= 2.40.1-3), + libpcre3 (= 2:8.39-9), + libperl5.26 (= 5.26.1-5), + libpipeline1 (= 1.5.0-1), + libpixman-1-0 (= 0.34.0-2), + libpng16-16 (= 1.6.34-1), + libpoppler73 (= 0.62.0-2), + libpotrace0 (= 1.14-2), + libprocps6 (= 2:3.3.14-1), + libproxy1v5 (= 0.4.15-1), + libptexenc1 (= 2017.20170613.44572-8+b2), + libpython-stdlib (= 2.7.14-4), + libpython2.7-minimal (= 2.7.14-8), + libpython2.7-stdlib (= 2.7.14-8), + libpython3-stdlib (= 3.6.5-3), + libpython3.6-minimal (= 3.6.5-3), + libpython3.6-stdlib (= 3.6.5-3), + libquadmath0 (= 8-20180402-1), + libreadline7 (= 7.0-3), + librest-0.7-0 (= 0.8.0-2), + librsvg2-2 (= 2.40.20-2), + librsvg2-bin (= 2.40.20-2), + librsvg2-common (= 2.40.20-2), + libseccomp2 (= 2.3.1-2.1), + libselinux1 (= 2.7-2+b2), + libsemanage-common (= 2.7-2), + libsemanage1 (= 2.7-2+b2), + libsepol1 (= 2.7-1), + libsigsegv2 (= 2.12-2), + libsm6 (= 2:1.2.2-1+b3), + libsmartcols1 (= 2.31.1-0.5), + libsoup-gnome2.4-1 (= 2.62.1-1), + libsoup2.4-1 (= 2.62.1-1), + libsqlite3-0 (= 3.23.1-1), + libss2 (= 1.44.1-2), + libssl1.1 (= 1.1.0h-2), + libstdc++-7-dev (= 7.3.0-16), + libstdc++6 (= 8-20180402-1), + libsynctex1 (= 2017.20170613.44572-8+b2), + libsystemd0 (= 238-4), + libtasn1-6 (= 4.13-2), + libtexlua52 (= 2017.20170613.44572-8+b2), + libtexluajit2 (= 2017.20170613.44572-8+b2), + libtext-unidecode-perl (= 1.30-1), + libthai-data (= 0.1.27-2), + libthai0 (= 0.1.27-2), + libtiff5 (= 4.0.9-4), + libtimedate-perl (= 2.3000-2), + libtinfo5 (= 6.1-1), + libtool (= 2.4.6-2), + libtsan0 (= 8-20180402-1), + libubsan0 (= 7.3.0-16), + libudev1 (= 238-4), + libunistring2 (= 0.9.8-1), + libuuid1 (= 2.31.1-0.5), + libwayland-client0 (= 1.14.0-2), + libwayland-cursor0 (= 1.14.0-2), + libwayland-egl1-mesa (= 17.3.8-1), + libwayland-server0 (= 1.14.0-2), + libwebp6 (= 0.6.1-2), + libx11-6 (= 2:1.6.5-1), + libx11-data (= 2:1.6.5-1), + libx11-xcb1 (= 2:1.6.5-1), + libxau6 (= 1:1.0.8-1+b2), + libxaw7 (= 2:1.0.13-1+b2), + libxcb-dri2-0 (= 1.13-1), + libxcb-dri3-0 (= 1.13-1), + libxcb-present0 (= 1.13-1), + libxcb-render0 (= 1.13-1), + libxcb-shm0 (= 1.13-1), + libxcb-sync1 (= 1.13-1), + libxcb-xfixes0 (= 1.13-1), + libxcb1 (= 1.13-1), + libxcomposite1 (= 1:0.4.4-2), + libxcursor1 (= 1:1.1.15-1), + libxdamage1 (= 1:1.1.4-3), + libxdmcp6 (= 1:1.1.2-3), + libxdot4 (= 2.40.1-3), + libxext6 (= 2:1.3.3-1+b2), + libxfixes3 (= 1:5.0.3-1), + libxi6 (= 2:1.7.9-1), + libxinerama1 (= 2:1.1.3-1+b3), + libxkbcommon0 (= 0.8.0-1), + libxml-libxml-perl (= 2.0128+dfsg-5), + libxml-namespacesupport-perl (= 1.12-1), + libxml-sax-base-perl (= 1.09-1), + libxml-sax-perl (= 1.00+dfsg-1), + libxml2 (= 2.9.4+dfsg1-6.1), + libxmu6 (= 2:1.1.2-2), + libxpm4 (= 1:3.5.12-1), + libxrandr2 (= 2:1.5.1-1), + libxrender1 (= 1:0.9.10-1), + libxshmfence1 (= 1.3-1), + libxt6 (= 1:1.1.5-1), + libzzip-0-13 (= 0.13.62-3.1), + linux-libc-dev (= 4.15.11-1), + login (= 1:4.5-1), + lsb-base (= 9.20170808), + m4 (= 1.4.18-1), + make (= 4.2.1-1), + man-db (= 2.8.3-2), + mawk (= 1.3.3-17+b3), + mime-support (= 3.60), + mount (= 2.31.1-0.5), + ncurses-base (= 6.1-1), + ncurses-bin (= 6.1-1), + openssl (= 1.1.0h-2), + passwd (= 1:4.5-1), + patch (= 2.7.6-2), + perl (= 5.26.1-5), + perl-base (= 5.26.1-5), + perl-modules-5.26 (= 5.26.1-5), + po-debconf (= 1.0.20), + poppler-data (= 0.4.8-2), + preview-latex-style (= 11.91-1), + procps (= 2:3.3.14-1), + python (= 2.7.14-4), + python-alabaster (= 0.7.8-1), + python-all (= 2.7.14-4), + python-attr (= 17.4.0-2), + python-babel (= 2.4.0+dfsg.1-2), + python-babel-localedata (= 2.4.0+dfsg.1-2), + python-certifi (= 2018.1.18-3), + python-chardet (= 3.0.4-1), + python-docutils (= 0.14+dfsg-3), + python-enum34 (= 1.1.6-2), + python-funcsigs (= 1.0.2-4), + python-html5lib (= 0.999999999-1), + python-idna (= 2.6-1), + python-imagesize (= 0.7.1-1), + python-jinja2 (= 2.10-1), + python-markupsafe (= 1.0-1+b1), + python-minimal (= 2.7.14-4), + python-mock (= 2.0.0-3), + python-packaging (= 17.1-1), + python-pbr (= 3.1.1-4), + python-pkg-resources (= 39.0.1-2), + python-pluggy (= 0.6.0-1), + python-py (= 1.5.3-1), + python-pygments (= 2.2.0+dfsg-1), + python-pyparsing (= 2.2.0+dfsg1-2), + python-pytest (= 3.3.2-2), + python-requests (= 2.18.4-2), + python-roman (= 2.0.0-3), + python-setuptools (= 39.0.1-2), + python-six (= 1.11.0-2), + python-typing (= 3.6.4-1), + python-tz (= 2018.4-1), + python-urllib3 (= 1.22-1), + python-webencodings (= 0.5-2), + python2.7 (= 2.7.14-8), + python2.7-minimal (= 2.7.14-8), + python3 (= 3.6.5-3), + python3-alabaster (= 0.7.8-1), + python3-all (= 3.6.5-3), + python3-attr (= 17.4.0-2), + python3-babel (= 2.4.0+dfsg.1-2), + python3-certifi (= 2018.1.18-3), + python3-chardet (= 3.0.4-1), + python3-distutils (= 3.6.5-3), + python3-docutils (= 0.14+dfsg-3), + python3-html5lib (= 0.999999999-1), + python3-idna (= 2.6-1), + python3-imagesize (= 0.7.1-1), + python3-jinja2 (= 2.10-1), + python3-lib2to3 (= 3.6.5-3), + python3-markupsafe (= 1.0-1+b1), + python3-minimal (= 3.6.5-3), + python3-mock (= 2.0.0-3), + python3-packaging (= 17.1-1), + python3-pbr (= 3.1.1-4), + python3-pkg-resources (= 39.0.1-2), + python3-pluggy (= 0.6.0-1), + python3-py (= 1.5.3-1), + python3-pygments (= 2.2.0+dfsg-1), + python3-pyparsing (= 2.2.0+dfsg1-2), + python3-pytest (= 3.3.2-2), + python3-requests (= 2.18.4-2), + python3-roman (= 2.0.0-3), + python3-setuptools (= 39.0.1-2), + python3-six (= 1.11.0-2), + python3-sphinx (= 1.6.7-2), + python3-sphinxcontrib.websupport (= 1.0.1-2), + python3-tz (= 2018.4-1), + python3-urllib3 (= 1.22-1), + python3-webencodings (= 0.5-2), + python3.6 (= 3.6.5-3), + python3.6-minimal (= 3.6.5-3), + readline-common (= 7.0-3), + sed (= 4.4-2), + sensible-utils (= 0.0.12), + sgml-base (= 1.29), + shared-mime-info (= 1.9-2), + sphinx-common (= 1.6.7-2), + systemd (= 238-4), + systemd-shim (= 10-3), + sysvinit-utils (= 2.88dsf-59.10), + t1utils (= 1.41-2), + tar (= 1.29b-2), + tex-common (= 6.09), + texinfo (= 6.5.0.dfsg.1-2), + texlive-base (= 2017.20180305-1), + texlive-binaries (= 2017.20170613.44572-8+b2), + texlive-fonts-recommended (= 2017.20180305-1), + texlive-latex-base (= 2017.20180305-1), + texlive-latex-extra (= 2017.20180305-2), + texlive-latex-recommended (= 2017.20180305-1), + texlive-luatex (= 2017.20180305-1), + texlive-pictures (= 2017.20180305-1), + texlive-xetex (= 2017.20180305-1), + tipa (= 2:1.3-20), + tzdata (= 2018d-1), + ucf (= 3.0038), + util-linux (= 2.31.1-0.5), + x11-common (= 1:7.7+19), + xdg-utils (= 1.1.2-2), + xkb-data (= 2.23.1-1), + xml-core (= 0.18), + xz-utils (= 5.2.2-1.3), + zlib1g (= 1:1.2.8.dfsg-5) +Environment: + DEB_BUILD_OPTIONS="parallel=9" + DEB_BUILD_PROFILES="" + SOURCE_DATE_EPOCH="1523656345" + +-----BEGIN PGP SIGNATURE----- + +iQIzBAEBCgAdFiEEQw+hF5tfsLeq16ge4J9rT55v3MsFAmFV/j8ACgkQ4J9rT55v +3MvXSA/8D9NLhB8NsNFTPxrz3v0gErdlUwUyFvEXMsr9O0pdURyhO2YX5aeA3lZ4 +HCYijsEE/9OC+tNNhXMKRiZE7/GWNs/cFoZ9ylEWdX56eITzpjlDzT/IIvqDt0jj +y06YzvNsb91eb9Gq1+Lr64jGNqTgVVY5EKJBKeawUaCadyE1ZndwsnFOlFofRQ6N +KoCTSEeIj5ZzXpnOG9FRC7qw7dSp/T/GN3miP/YDs1UtI+DI02mTmlYKQgQWyWSS +WIhIqyjuEhtxJx8dyzB36OJi/UngRT2Gdb6lze+sQlzRelB6yV9+mWPmwTHhyCET +j4vKL2I8lbRz+L4/NtUpH1fEcvgyhCeg0+TInpMh9fskcjbOX2k+e44Yv5NqeuWN ++ebpnS5sz+ubdwtHhiEJQxvw6rft5REatr7mWt13ZtGIt9BcAzvTjp13OZxKnaNc +6vSi9LMnwFP61NMGsV97miUlRuE5zOry3Rev3yIBA14fI8jt5c2Gnlh5GigEro2B +LkhjvHigPyZPWUk+TMnLqdUeiTaK3b1zZfkRcUN865e9H81uByF3Sed6rcfO2OrY +gQUKC2aofknKMQCxGlHNISGCC2qS5trzHq1rEUx094gQpB45cfjRa3VDiGPu6Uaz +WMeENcRyo+2kLVZTxjA2TqGTUjiE08f2+aRJnexTfp+KyMS+E30= +=GYb3 +-----END PGP SIGNATURE----- diff --git a/signer/gpg2/test/fixtures/sphinx_1.7.2-1_amd64.changes b/signer/gpg2/test/fixtures/sphinx_1.7.2-1_amd64.changes new file mode 100644 index 000000000..7815cbf9d --- /dev/null +++ b/signer/gpg2/test/fixtures/sphinx_1.7.2-1_amd64.changes @@ -0,0 +1,74 @@ +-----BEGIN PGP SIGNED MESSAGE----- +Hash: SHA512 + +Format: 1.8 +Date: Fri, 13 Apr 2018 22:52:25 +0100 +Source: sphinx +Binary: python-sphinx python3-sphinx sphinx-common sphinx-doc libjs-sphinxdoc +Architecture: source all +Version: 1.7.2-1 +Distribution: UNRELEASED +Urgency: medium +Maintainer: Debian Python Modules Team +Changed-By: Chris Lamb +Description: + libjs-sphinxdoc - JavaScript support for Sphinx documentation + python-sphinx - documentation generator for Python projects (implemented in Pytho + python3-sphinx - documentation generator for Python projects (implemented in Pytho + sphinx-common - documentation generator for Python projects - common data + sphinx-doc - documentation generator for Python projects - documentation +Changes: + sphinx (1.7.2-1) UNRELEASED; urgency=medium + . + [ Chris Lamb ] + * New upstream release. + . + [ Dmitry Shachnev ] + * Merge 1.6.7-2 upload from unstable. +Checksums-Sha1: + 3c7ba10ca6e46b80044fea71f2486d98c4aebbd1 3754 sphinx_1.7.2-1.dsc + 1d1fa6954ae216cd44ea52dfc67063f26939c8f5 4719536 sphinx_1.7.2.orig.tar.gz + facfa686a3a0bc98c269e16e66427f96e00889ad 34268 sphinx_1.7.2-1.debian.tar.xz + 36a42125afd83731e05596d7037314399622e400 88720 libjs-sphinxdoc_1.7.2-1_all.deb + 8c2ecdf4e4ffd1f4880b486e53c99207c4d69dbc 443380 python-sphinx_1.7.2-1_all.deb + e518d97dceb77d827734d48ac02313fc0ed4b08c 441676 python3-sphinx_1.7.2-1_all.deb + 01e65d2e2c45121e06aff6f46427d6a31251e67c 432892 sphinx-common_1.7.2-1_all.deb + 8bec16b4db2ddf6776f7cfdcf04cd07fe32269ed 1198352 sphinx-doc_1.7.2-1_all.deb + 9c3194ba6dab7409b0e95b76ab5bd5cd6da21fd8 15238 sphinx_1.7.2-1_amd64.buildinfo +Checksums-Sha256: + 01b74eed4619d52a75707961004f738b6985bd993962b948645dcd061235fe66 3754 sphinx_1.7.2-1.dsc + 5a1c9a0fec678c24b9a2f5afba240c04668edb7f45c67ce2ed008996b3f21ae2 4719536 sphinx_1.7.2.orig.tar.gz + a6a825914b19cfdbc22df858b0cecc497765dad2058deae20a88a6a2f9d57d24 34268 sphinx_1.7.2-1.debian.tar.xz + c6267177819e0a27b8f9296b1d5106e253e84cf0722fbdf8d2bff489befb81d8 88720 libjs-sphinxdoc_1.7.2-1_all.deb + c55dc8f59798a835e6f78adfd95d73f9cf3382e440cdf96b2edbdb1990280f23 443380 python-sphinx_1.7.2-1_all.deb + 3f2b279b572d2fc36d9cc84c9fcded242ed930d42350e1d980c4aed45ee4ae2a 441676 python3-sphinx_1.7.2-1_all.deb + ece661af9ca87723169f48e68a45af0b5157da4b724a185b99ef42ed22e5f378 432892 sphinx-common_1.7.2-1_all.deb + 67722aa0934b9ebce8a870a34542104017491766c2515665c07128ad46b4e6b7 1198352 sphinx-doc_1.7.2-1_all.deb + f3e9782a2995a51a91a3c51ab0a306fc1f61c1b3d92d488aa8e5fbb9d8fe2d30 15238 sphinx_1.7.2-1_amd64.buildinfo +Files: + db1177999615f0aaeed19bf8fc850fc9 3754 python optional sphinx_1.7.2-1.dsc + 21a08e994e6a289ed14eecefde2b4f2f 4719536 python optional sphinx_1.7.2.orig.tar.gz + e147e2afa47e7e58d1288ad8818c3de0 34268 python optional sphinx_1.7.2-1.debian.tar.xz + 4a4df0c2ce6f087414a5dc5802885fd2 88720 javascript optional libjs-sphinxdoc_1.7.2-1_all.deb + 15b065a197e938715bf501b33066adf2 443380 python optional python-sphinx_1.7.2-1_all.deb + 942189b1dd93faa36faa3552a9d40042 441676 python optional python3-sphinx_1.7.2-1_all.deb + f6a5889bd4e807800239721d365a5f62 432892 python optional sphinx-common_1.7.2-1_all.deb + 86497c90b6cda029576933778357004e 1198352 doc optional sphinx-doc_1.7.2-1_all.deb + fb16629356705f593b80b2df8fa8fa19 15238 python optional sphinx_1.7.2-1_amd64.buildinfo + +-----BEGIN PGP SIGNATURE----- + +iQIzBAEBCgAdFiEEQw+hF5tfsLeq16ge4J9rT55v3MsFAmFV/kEACgkQ4J9rT55v +3Mvx5xAAlHMdr9vwp3LCAWwW4flbhhYtDyJo5fSPUAK5L32BhGeXBsMQr2nVzLWC +lqzHIHbjIaeznsGCuv0FJ03bU8GHHlpkBa2URLOedcR6ylGjUjphtB7rZaPplryY +K2piL5krdCfr/HzxOWxulJNTWApe5/OO81gIZLVOTGqMlCtgb7Qy8a+lT0ZNEEYq +gpIyakx6RwbJ8zahhdEV8gXOR958HuBTMy/2TICbdJgtyvcdeJMSN9t+Azw+AZNK +6CR0i42A4O8IojV7O6KDoLrVNpb4/0RMBYEyLqjp4IFRblPxma2Ar/0+sGeaqZUc +2qj/qQIqNOyf7M0E0R4y9C1WUkIywL5Pxf67O9I2AaTz9WjO8Tbc+1NRVozTn8Wi +rrarkAEzilXwWAM7JaIw0vDR2zGm7Eggmlsuz+XwrkAkt5XbxjDgWN902lg+1B8+ +9tnFtjbvVQATTnJ1qWzNV78klQaRbBPM+aH720yTTUjplUYZiyUvRd3QuEPl1m/b +7Gs+F4WnhnLVD0DZfN1SM9K6HX5X+troqqwmSSewu3Xb1+AX1Hgpr8hIOS8q4W7k +cExvpihedWoKtTlXX4eRHJrx2tSiJXQ/bO8k/ArPqigsZTUIFTU46QJsoMnlKRI+ +npB7Z4KvRd3nHl+9A/Ulwnyi0nqiZlA1M9DOcL2iXwAwRZSv3kY= +=i0IL +-----END PGP SIGNATURE----- diff --git a/tools/autograph-client/build_test_gpg.sh b/tools/autograph-client/build_test_gpg.sh new file mode 100755 index 000000000..29c43c851 --- /dev/null +++ b/tools/autograph-client/build_test_gpg.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash + +set -e +set -o pipefail + +# test script to check default gpg2 signers sign and verify test data +# or debsign files +# +# when no args are provided it signs fixed test data (gpg2 signer +# should be in gpg2 mode) +# when a list of debsign files is provided it signs them in debsign mode + +HAWK_USER=${HAWK_USER:-alice} +HAWK_SECRET=${HAWK_SECRET:-fs5wgcer9qj819kfptdlp8gm227ewxnzvsuj9ztycsx08hfhzu} +TARGET=${TARGET:-'http://127.0.0.1:8000'} +SIGNER_ID=${SIGNER_ID:?} +VERIFY=${VERIFY:-"0"} + +DEBSIGN_FILES=( "$@" ) + +test_dir=$(mktemp -d --suffix "$SIGNER_ID") + +if [ "$#" -eq 0 ]; then + echo "testing signing data with signer: ${SIGNER_ID} in ${test_dir}" + + echo 'hello' | base64 > "${test_dir}/pgpinput.txt" + + go run client.go -t "$TARGET" -u "$HAWK_USER" -p "$HAWK_SECRET" -k "$SIGNER_ID" -d "$(cat "${test_dir}"/pgpinput.txt)" -o "${test_dir}/testsig.pgp" -ko "${test_dir}/testkey.asc" + + if [ "$VERIFY" = "1" ]; then + # import the public key returned by autograph into a temp keyring + gpg --no-options --homedir "${test_dir}" --no-default-keyring --keyring "${test_dir}/testkeyring.pgp" --secret-keyring "${test_dir}/testsecring.gpg" --import "${test_dir}/testkey.asc" + + # verify the signature using the temp keyring + echo "running: gpg --no-options --homedir \"${test_dir}\" --no-default-keyring --keyring \"${test_dir}/testkeyring.pgp\" --verify \"${test_dir}/testsig.pgp\" <(base64 -d \"${test_dir}/pgpinput.txt\")" + gpg --no-options --homedir "${test_dir}" --no-default-keyring --keyring "${test_dir}/testkeyring.pgp" --verify "${test_dir}/testsig.pgp" <(base64 -d "${test_dir}/pgpinput.txt") + fi +else + echo "testing signing files " "${DEBSIGN_FILES[@]}" " with signer: ${SIGNER_ID} in ${test_dir}" + cd "$test_dir" + autograph-client -t "$TARGET" -u "$HAWK_USER" -p "$HAWK_SECRET" -k "$SIGNER_ID" -ko "${test_dir}/testkey.asc" -outfilesprefix signed_ "${DEBSIGN_FILES[@]}" + + if [ "$VERIFY" = "1" ]; then + # import the public key returned by autograph into a temp keyring + gpg --no-options --homedir "${test_dir}" --no-default-keyring --keyring "${test_dir}/testkeyring.pgp" --secret-keyring "${test_dir}/testsecring.gpg" --import "${test_dir}/testkey.asc" + + # verify the signature using the temp keyring + for signed in signed_*; do + echo "verifying gpg2 clearsign signature for ${signed}" + echo "running: gpg --no-options --homedir \"${test_dir}\" --no-default-keyring --keyring \"${test_dir}/testkeyring.pgp\" --verify \"$signed\"" + gpg --no-options --homedir "${test_dir}" --no-default-keyring --keyring "${test_dir}/testkeyring.pgp" --verify "$signed" + done + fi + cd "$OLDPWD" +fi diff --git a/tools/autograph-client/client.go b/tools/autograph-client/client.go index 54ad1d325..8ef47eae4 100644 --- a/tools/autograph-client/client.go +++ b/tools/autograph-client/client.go @@ -14,6 +14,7 @@ import ( "log" "net/http" "os" + "path/filepath" "strings" "time" @@ -36,6 +37,7 @@ const ( requestTypeData requestTypeHash requestTypeFile + requestTypeFiles ) type coseAlgs []string @@ -56,6 +58,8 @@ func urlToRequestType(url string) requestType { return requestTypeHash } else if strings.HasSuffix(url, "/sign/file") { return requestTypeFile + } else if strings.HasSuffix(url, "/sign/files") { + return requestTypeFiles } log.Fatalf("Unrecognized request type for url %q", url) return requestTypeNone @@ -63,13 +67,13 @@ func urlToRequestType(url string) requestType { func main() { var ( - userid, pass, data, hash, url, infile, outfile, outkeyfile, keyid, cn, pk7digest, rootPath, verificationTimeInput string - iter, maxworkers, sa int - debug, listKeyIDs, noVerify bool - err error - requests []formats.SignatureRequest - algs coseAlgs - verificationTime time.Time + userid, pass, data, hash, url, infile, outfile, outkeyfile, outFilesPrefix, keyid, cn, pk7digest, rootPath, verificationTimeInput string + iter, maxworkers, sa int + debug, listKeyIDs, noVerify bool + err error + requests []formats.SignatureRequest + algs coseAlgs + verificationTime time.Time ) flag.Usage = func() { fmt.Print("autograph-client - command line client to the autograph service\n\n") @@ -122,6 +126,9 @@ examples: * sign some data with gpg2: $ go run client.go -d $(echo 'hello' | base64) -k pgpsubkey -o /tmp/testsig.pgp -ko /tmp/testkey.asc +* sign some files with debsign and write signed output files to signed_foo_*: + $ go run client.go -k pgpsubkey-debsign -outfilesprefix signed_foo_ foo.dsc foo.buildinfo foo.changes + * sign SHA1 hashed data with rsa pss: $ go run client.go -D -a $(echo hi | sha1sum -b | cut -d ' ' -f 1 | xxd -r -p | base64) -k dummyrsapss -o signed-hash.out -ko /tmp/testkey.pub @@ -136,6 +143,7 @@ examples: flag.StringVar(&infile, "f", "/path/to/file", "Input file to sign, will use the /sign/file endpoint") flag.StringVar(&outfile, "o", ``, "Output file. If set, writes the signature or file to this location") flag.StringVar(&outkeyfile, "ko", ``, "Key Output file. If set, writes the public key to a file at this location") + flag.StringVar(&outFilesPrefix, "outfilesprefix", `signed_`, "Prefix to use for output filenames when signing multiple files. Defaults to 'signed_'") flag.StringVar(&keyid, "k", ``, "Key ID to request a signature from a specific signer") flag.StringVar(&url, "t", `http://localhost:8000`, "target server, do not specific a URI or trailing slash") flag.IntVar(&iter, "i", 1, "number of signatures to request") @@ -170,6 +178,10 @@ examples: os.Exit(0) } + var ( + inputFiles []formats.SigningFile + request formats.SignatureRequest + ) if data != "base64(data)" { log.Printf("signing data %q", data) url = url + "/sign/data" @@ -185,11 +197,31 @@ examples: log.Fatal(err) } data = base64.StdEncoding.EncodeToString(filebytes) + } else { + log.Printf("signing files %q", flag.Args()) + url = url + "/sign/files" + for _, inputFilename := range flag.Args() { + inputFileBytes, err := ioutil.ReadFile(inputFilename) + if err != nil { + log.Fatal(err) + } + inputFiles = append(inputFiles, formats.SigningFile{ + Name: filepath.Base(inputFilename), + Content: base64.StdEncoding.EncodeToString(inputFileBytes), + }) + } } - request := formats.SignatureRequest{ - Input: data, - KeyID: keyid, + if strings.HasSuffix(url, "/sign/files") { + request = formats.SignatureRequest{ + Files: inputFiles, + KeyID: keyid, + } + } else { + request = formats.SignatureRequest{ + Input: data, + KeyID: keyid, + } } // if signing an xpi, the CN, COSEAlgorithms, and PKCS7Digest are set in the options if cn != "" { @@ -279,14 +311,18 @@ examples: } reqType := urlToRequestType(url) for i, response := range responses { - input, err := base64.StdEncoding.DecodeString(requests[i].Input) - if err != nil { - log.Fatal(err) - } var ( - sigStatus bool - sigData []byte + input []byte + signedFiles []formats.SigningFile + sigStatus bool + sigData []byte ) + if reqType != requestTypeFiles { + input, err = base64.StdEncoding.DecodeString(requests[i].Input) + if err != nil { + log.Fatal(err) + } + } switch response.Type { case contentsignature.Type: if !noVerify { @@ -348,10 +384,18 @@ examples: log.Fatal(err) } case gpg2.Type: - if !noVerify { - sigStatus = verifyPGP(input, response.Signature, response.PublicKey) + if reqType == requestTypeFiles { + // TODO: implement verify pgp clearsigned + if !noVerify { + sigStatus = true + } + signedFiles = response.SignedFiles + } else { + if !noVerify { + sigStatus = verifyPGP(input, response.Signature, response.PublicKey) + } + sigData = []byte(response.Signature) } - sigData = []byte(response.Signature) default: log.Fatalf("unsupported signature type: %s", response.Type) } @@ -376,6 +420,18 @@ examples: } log.Println("public key written to", outkeyfile) } + for _, signedFile := range signedFiles { + signedOutputFilename := fmt.Sprintf("%s%s", outFilesPrefix, signedFile.Name) + signedFileBytes, err := base64.StdEncoding.DecodeString(signedFile.Content) + if err != nil { + log.Fatal(err) + } + err = ioutil.WriteFile(signedOutputFilename, signedFileBytes, 0644) + if err != nil { + log.Fatal(err) + } + log.Printf("wrote signed file %s", signedOutputFilename) + } } workers-- }() diff --git a/tools/autograph-client/integration_test_gpg2_signer.sh b/tools/autograph-client/integration_test_gpg2_signer.sh new file mode 100755 index 000000000..93c678518 --- /dev/null +++ b/tools/autograph-client/integration_test_gpg2_signer.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env sh + +set -e + + +AUTOGRAPH_URL=${AUTOGRAPH_URL:?} + +SIGNER_ID=randompgp \ + TARGET="$AUTOGRAPH_URL" \ + VERIFY=1 \ + ./build_test_gpg.sh + +SIGNER_ID=pgpsubkey \ + TARGET="$AUTOGRAPH_URL" \ + VERIFY=1 \ + ./build_test_gpg.sh + +SIGNER_ID=randompgp-debsign \ + TARGET="$AUTOGRAPH_URL" \ + VERIFY=1 \ + ./build_test_gpg.sh /app/src/autograph/signer/gpg2/test/fixtures/sphinx_* + +SIGNER_ID=pgpsubkey-debsign \ + TARGET="$AUTOGRAPH_URL" \ + VERIFY=1 \ + ./build_test_gpg.sh /app/src/autograph/signer/gpg2/test/fixtures/sphinx_* From 93148bba5f71b5414b82d376e58b8b444770f272 Mon Sep 17 00:00:00 2001 From: Greg Guthe Date: Thu, 7 Oct 2021 17:15:56 -0400 Subject: [PATCH 09/11] signer: gpg2: error when cleaning up keys temp files to avoid leaving them on disk --- signer/gpg2/gpg2.go | 48 +++++++++++++++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/signer/gpg2/gpg2.go b/signer/gpg2/gpg2.go index 73f943e39..3a3e3cae0 100644 --- a/signer/gpg2/gpg2.go +++ b/signer/gpg2/gpg2.go @@ -151,12 +151,15 @@ func New(conf signer.Configuration) (s *GPG2Signer, err error) { // createKeyRing creates a temporary gpg sec and keyrings, loads the // private and public keys for the signer, and returns the temporary -// director holding the rings -func createKeyRing(s *GPG2Signer) (string, error) { +// directory holding the key rings. +// +// It return errors for any failure to create the tmp dir; or write, +// import, or clean up the private and public keys. +func createKeyRing(s *GPG2Signer) (dir string, err error) { // reuse keyring in tempdir prefix := fmt.Sprintf("autograph_%s_%s_%s_", s.Type, s.KeyID, s.Mode) - dir, err := ioutil.TempDir("", prefix) + dir, err = ioutil.TempDir("", prefix) if err != nil { return "", fmt.Errorf("gpg2: error creating tempdir for keyring: %w", err) } @@ -164,23 +167,40 @@ func createKeyRing(s *GPG2Signer) (string, error) { // write the public key to a temp file in our signer's temp dir tmpPublicKeyFile, err := ioutil.TempFile(dir, "gpg2_publickey") if err != nil { - return "", fmt.Errorf("gpg2: error creating tempfile for public key: %w", err) - } - defer os.Remove(tmpPublicKeyFile.Name()) + err = fmt.Errorf("gpg2: error creating tempfile for public key: %w", err) + return "", err + } + defer func() { + cleanErr := os.Remove(tmpPublicKeyFile.Name()) + // only clobber the original error when it's nil + if err == nil && cleanErr != nil { + err = fmt.Errorf("gpg2: error removing temp pubkey file %q: %w", tmpPublicKeyFile.Name(), cleanErr) + } + }() + err = ioutil.WriteFile(tmpPublicKeyFile.Name(), []byte(s.PublicKey), 0755) if err != nil { - return "", fmt.Errorf("gpg2: error writing public key to tempfile: %w", err) + err = fmt.Errorf("gpg2: error writing public key to tempfile: %w", err) + return "", err } // write the private key to a temp file in our signer's temp dir tmpPrivateKeyFile, err := ioutil.TempFile(dir, "gpg2_privatekey") if err != nil { - return "", fmt.Errorf("gpg2: error creating tempfile for private key: %w", err) - } - defer os.Remove(tmpPrivateKeyFile.Name()) + err = fmt.Errorf("gpg2: error creating tempfile for private key: %w", err) + return "", err + } + defer func() { + cleanErr := os.Remove(tmpPrivateKeyFile.Name()) + // only clobber the original error when it's nil + if err == nil && cleanErr != nil { + err = fmt.Errorf("gpg2: error removing temp private key file %q %w", tmpPrivateKeyFile.Name(), cleanErr) + } + }() err = ioutil.WriteFile(tmpPrivateKeyFile.Name(), []byte(s.PrivateKey), 0755) if err != nil { - return "", fmt.Errorf("gpg2: error writing private key to tempfile: %w", err) + err = fmt.Errorf("gpg2: error writing private key to tempfile: %w", err) + return "", err } keyRingPath := filepath.Join(dir, keyRingFilename) @@ -202,7 +222,8 @@ func createKeyRing(s *GPG2Signer) (string, error) { gpgLoadPublicKey.Dir = dir out, err := gpgLoadPublicKey.CombinedOutput() if err != nil { - return "", fmt.Errorf("gpg2: failed to load public key into keyring: %s\n%s", err, out) + err = fmt.Errorf("gpg2: failed to load public key into keyring: %s\n%s", err, out) + return "", err } log.Debugf("gpg2: loaded public key %s", string(out)) @@ -220,7 +241,8 @@ func createKeyRing(s *GPG2Signer) (string, error) { gpgLoadPrivateKey.Dir = dir out, err = gpgLoadPrivateKey.CombinedOutput() if err != nil { - return "", fmt.Errorf("gpg2: failed to load private key into keyring: %s\n%s", err, out) + err = fmt.Errorf("gpg2: failed to load private key into keyring: %s\n%s", err, out) + return "", err } log.Debugf("gpg2: loaded private key %s", string(out)) From 9c20a384be58b03d2e47aaec3a3b99906ceb359e Mon Sep 17 00:00:00 2001 From: Greg Guthe Date: Thu, 7 Oct 2021 17:16:48 -0400 Subject: [PATCH 10/11] signer: gpg2: warn when cleaning up temp files and dirs after signing fails --- signer/gpg2/gpg2.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/signer/gpg2/gpg2.go b/signer/gpg2/gpg2.go index 3a3e3cae0..db33e8fb8 100644 --- a/signer/gpg2/gpg2.go +++ b/signer/gpg2/gpg2.go @@ -299,7 +299,11 @@ func (s *GPG2Signer) SignData(data []byte, options interface{}) (signer.Signatur if err != nil { return nil, fmt.Errorf("gpg2: failed to create tempfile for input to sign: %w", err) } - defer os.Remove(tmpContentFile.Name()) + defer func() { + if err := os.Remove(tmpContentFile.Name()); err != nil { + log.Warnf("gpg2: error removing content file %q: %q", tmpContentFile.Name(), err) + } + }() err = ioutil.WriteFile(tmpContentFile.Name(), data, 0755) if err != nil { return nil, fmt.Errorf("gpg2: failed to write tempfile for input to sign: %w", err) @@ -391,7 +395,11 @@ func (s *GPG2Signer) SignFiles(inputs []signer.NamedUnsignedFile, options interf err = fmt.Errorf("gpg2: error creating tempdir for debsign: %w", err) return } - defer os.RemoveAll(inputsTmpDir) + defer func() { + if err := os.RemoveAll(inputsTmpDir); err != nil { + log.Warnf("gpg2: error removing sign files inputs directory %q: %q", inputsTmpDir, err) + } + }() // write the inputs to their tmp dir var inputFilePaths []string From 25589ced49e10ba317ac197d5b5ad9b32da3526d Mon Sep 17 00:00:00 2001 From: Greg Guthe Date: Fri, 8 Oct 2021 09:50:35 -0400 Subject: [PATCH 11/11] signer: gpg2: test tempfs side effects --- signer/gpg2/gpg2_test.go | 77 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/signer/gpg2/gpg2_test.go b/signer/gpg2/gpg2_test.go index 2a489a9f0..b1353e049 100644 --- a/signer/gpg2/gpg2_test.go +++ b/signer/gpg2/gpg2_test.go @@ -22,6 +22,48 @@ func assertNewSignerWithConfOK(t *testing.T, conf signer.Configuration) *GPG2Sig if err != nil { t.Fatalf("signer initialization failed with: %v", err) } + + matches, err := filepath.Glob(filepath.Join(s.tmpDir, "*")) + if err != nil { + t.Fatalf("signer initialization failed to find files in temp dir: %v", err) + } + // t.Logf("found files %s", matches) + + // check keyring exists + foundKeyring := false + for _, filename := range matches { + if filepath.Base(filename) == keyRingFilename { + foundKeyring = true + } + } + if !foundKeyring { + t.Fatalf("signer initialization failed to create keyring in signer temp dir") + } + + // check for gpg.conf written for debsign + if s.Mode == ModeDebsign { + foundConf := false + for _, filename := range matches { + if filepath.Base(filename) == gpgConfFilename { + foundConf = true + } + } + if !foundConf { + t.Fatalf("signer initialization failed to create gpg.conf in signer temp dir for debsign") + } + } + + // check private key is not left on disk + for _, filename := range matches { + matched, err := filepath.Match("gpg2_privatekey*", filepath.Base(filename)) + if err != nil { + t.Fatal(err) + } + if matched { + t.Fatalf("signer initialization failed to remove temp gpg private key: %s", filename) + } + } + return s } @@ -172,6 +214,26 @@ func TestNewSigner(t *testing.T) { }) } +func TestSignerAtExit(t *testing.T) { + t.Parallel() + + for _, conf := range validSignerConfigs { + t.Run(fmt.Sprintf("signer %s AtExit clean signer temp dir", conf.ID), func(t *testing.T) { + t.Parallel() + + s := assertNewSignerWithConfOK(t, conf) + if err := s.AtExit(); err != nil { + t.Fatal(err) + } + // check AtExit cleans up s.tmpDir + _, err := os.Stat(s.tmpDir) + if !os.IsNotExist(err) { + t.Fatalf("AtExit failed to clean temp dir %s", s.tmpDir) + } + }) + } +} + func TestConfig(t *testing.T) { t.Parallel() @@ -230,6 +292,13 @@ func TestSignData(t *testing.T) { if err != nil { t.Fatalf("failed to sign data: %v", err) } + matches, err := filepath.Glob(filepath.Join(s.tmpDir, "gpg2_*input*")) + if err != nil { + t.Fatal(err) + } + if len(matches) != 0 { + t.Fatalf("sign data did not clean up temp input files: %s", matches) + } // convert signature to string format sigstr, err := sig.Marshal() @@ -464,6 +533,14 @@ func TestGPG2Signer_SignFiles(t *testing.T) { assertClearSignedFilesVerify(t, s, "verify-debsigned-files", gotSignedFiles) }) } + + matches, err := filepath.Glob("/tmp/autograph_*sign_files*") + if err != nil { + t.Fatal(err) + } + if len(matches) != 0 { + t.Fatalf("sign files did not clean up its temp input directories: %s", matches) + } } // signer configs from the dev autograph.yaml