diff --git a/bin/run_integration_tests.sh b/bin/run_integration_tests.sh index b25763b95..7408e8463 100755 --- a/bin/run_integration_tests.sh +++ b/bin/run_integration_tests.sh @@ -24,13 +24,9 @@ while test "true" != "$(docker inspect -f {{.State.Running}} autograph-app-hsm)" sleep 1 # wait before checking again done -# fetch the updated root hash from the app-hsm service -docker cp autograph-app-hsm:/tmp/normandy_dev_root_hash.txt . -APP_HSM_NORMANDY_ROOT_HASH=$(grep '[0-9A-F]' normandy_dev_root_hash.txt | tr -d '\r\n') - # start the monitor lambda emulators docker compose up -d monitor-lambda-emulator -AUTOGRAPH_ROOT_HASH=$APP_HSM_NORMANDY_ROOT_HASH docker compose up -d monitor-hsm-lambda-emulator +docker compose up -d monitor-hsm-lambda-emulator echo "waiting for monitor-lambda-emulator to start" while test "true" != "$(docker inspect -f {{.State.Running}} autograph-monitor-lambda-emulator)"; do @@ -43,7 +39,6 @@ while test "true" != "$(docker inspect -f {{.State.Running}} autograph-monitor-h sleep 1 # wait before checking again done -echo "checking monitoring using hsm root hash:" "$APP_HSM_NORMANDY_ROOT_HASH" # exec in containers to workaround https://circleci.com/docs/2.0/building-docker-images/#accessing-services docker compose exec monitor-lambda-emulator "/usr/local/bin/test_monitor.sh" docker compose logs monitor-lambda-emulator diff --git a/bin/test_monitor.sh b/bin/test_monitor.sh old mode 100644 new mode 100755 diff --git a/docker-compose.yml b/docker-compose.yml index 4d91884f1..af856a88e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -82,7 +82,6 @@ services: - AUTOGRAPH_KEY=19zd4w3xirb5syjgdx8atq6g91m03bdsmzjifs2oddivswlu9qs # set a non-empty value to use the lambda handler - LAMBDA_TASK_ROOT=/usr/local/bin/ - - AUTOGRAPH_ROOT_HASH ports: - "9000:8080" links: @@ -103,7 +102,6 @@ services: - AUTOGRAPH_KEY=19zd4w3xirb5syjgdx8atq6g91m03bdsmzjifs2oddivswlu9qs # set a non-empty value to use the lambda handler - LAMBDA_TASK_ROOT=/usr/local/bin/ - - AUTOGRAPH_ROOT_HASH ports: - "9001:8080" links: diff --git a/docs/endpoints.md b/docs/endpoints.md index 7dbedd473..f92fb8415 100644 --- a/docs/endpoints.md +++ b/docs/endpoints.md @@ -380,3 +380,42 @@ Authorization: Hawk id="dh37fgj492je", ts="1353832234", nonce="j4h3g2", ext="som "webextensions-rsa-with-recommendation" ] ``` + +## /config/:keyid + +### Request + +Get the sanitized configuration of a signer. For example: + +```bash +GET /config/dummyrsa +Host: autograph.example.net +Authorization: Hawk id="dh37fgj492je", ts="1353832234", nonce="j4h3g2", ext="some-app-ext-data", mac="6R4rV5iE+NPoym+WwjeHzjAGXUtLNIxmo1vpMofpLAE=" +``` + +### Response + +400 Bad Request when the request includes a non-empty body +401 Unauthorized when the Authorization header is missing or HAWK authorization fails +404 Not Found when the keyid does not exist, or an authorization does not have permission to access the signer. +405 Method Not Allowed when the request method is not GET +200 OK when the authorization is valid and path and signer ID is found. Example response body with Content-Type application/json: + +```json +{ + "id": "dummyrsa", + "type": "genericrsa", + "mode": "pss", + "publickey": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtEM/Vdfd4Vl9wmeVdCYuWYnQl0Zc9RW5hLE4hFA+c277qanE8XCK+ap/c5so87XngLLfacB3zZhGxIOut/4SlEBOAUmVNCfnTO+YkRk3A8OyJ4XNqdn+/ov78ZbssGf+0zws2BcwZYwhtuTvro3yi62FQ7T1TpT5VjljH7sHW/iZnS/RKiY4DwqAN799gkB+Gwovtroabh2w5OX0P+PYyUbJLFQeo5uiAQ8cAXTlHqCkj11GYgU4ttVDuFGotKRyaRn1F+yKxE4LQcAULx7s0KzvS35mNU+MoywLWjy9a4TcjK0nq+BjspKX4UkNwVstvH18hQWun7E+dxTi59cRmwIDAQAB", + "hash": "sha256" +} +``` + +The returned configuration should be a subset of the internal configuration with the following differences: + - Public values, such as the `id`, `publickey` and `certificate` are copied verbatim. + - Private keys are hashed, and return only the SHA256 checksum of the secret value. + - The `certificate`, if present is parsed and the following additional fields are added: + + `cert_sha1`: Contains the SHA1 fingerprint of the DER certificate. + + `cert_sha256`: Contains the SHA256 fingerprint of the DER certificate. + + `cert_start`: Contains the certificate `NotBefore` time in RFC 3339 format. + + `cert_end`: Contains the certificate `NotAfter` time in RFC 3339 format. diff --git a/handlers.go b/handlers.go index 36c3b3033..a5ffb4086 100644 --- a/handlers.go +++ b/handlers.go @@ -495,3 +495,56 @@ func (a *autographer) handleGetAuthKeyIDs(w http.ResponseWriter, r *http.Request w.WriteHeader(http.StatusOK) w.Write(signerIDsJSON) } + +// handleGetConfig returns the public signer configuration (keyID param for the API) +func (a *autographer) handleGetConfig(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + httpError(w, r, http.StatusMethodNotAllowed, "%s method not allowed; endpoint accepts GET only", r.Method) + return + } + if r.Body != nil { + body, err := io.ReadAll(r.Body) + if err != nil { + httpError(w, r, http.StatusBadRequest, "failed to read request body: %s", err) + return + } + if len(body) > 0 { + httpError(w, r, http.StatusBadRequest, "endpoint received unexpected request body") + return + } + } + + pathKeyID, ok := mux.Vars(r)["keyid"] + if !ok { + httpError(w, r, http.StatusInternalServerError, "route is improperly configured") + return + } + if !signer.IDFormatRegexp.MatchString(pathKeyID) { + httpError(w, r, http.StatusBadRequest, "keyid in URL path '%s' is invalid, it must match %s", pathKeyID, signer.IDFormat) + return + } + _, headerAuthID, err := a.authorizeHeader(r) + if err != nil { + httpError(w, r, http.StatusUnauthorized, "authorization verification failed: %v", err) + return + } + + requestedSigner, err := a.authBackend.getSignerForUser(headerAuthID, pathKeyID) + if err != nil { + log.Infof("user authorization failed: %s", err) + httpError(w, r, http.StatusNotFound, "keyid %s was not found for user %s", pathKeyID, headerAuthID) + return + } + + requestedConfig := requestedSigner.Config() + signerConfigJSON, err := json.Marshal(requestedConfig.Sanitize()) + if err != nil { + log.Errorf("handleGetConfig failed to marshal JSON with error: %s", err) + httpError(w, r, http.StatusInternalServerError, "error marshaling response JSON") + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write(signerConfigJSON) +} diff --git a/handlers_test.go b/handlers_test.go index 1d5d4bca2..17c33c365 100644 --- a/handlers_test.go +++ b/handlers_test.go @@ -28,6 +28,7 @@ import ( "github.com/mozilla-services/autograph/database" "github.com/mozilla-services/autograph/formats" + "github.com/mozilla-services/autograph/signer" "github.com/mozilla-services/autograph/signer/apk2" "github.com/mozilla-services/autograph/signer/contentsignature" "github.com/mozilla-services/autograph/signer/xpi" @@ -37,6 +38,96 @@ import ( margo "go.mozilla.org/mar" ) +type HandlerTestCase struct { + name string + method string + url string + + // urlRouteVars are https://pkg.go.dev/github.com/gorilla/mux#Vars + // as configured with the handler at /config/{keyid:[a-zA-Z0-9-_]{1,64}} + // there should only be a keyid var and it should match the url value + urlRouteVars map[string]string + + // headers are additional http headers to set + headers *http.Header + + // user/auth ID to build an Authorization header for + authorizeID string + nilBody bool + body string + + expectedStatus int + expectedHeaders http.Header + expectedBody string +} + +func (testcase *HandlerTestCase) NewRequest(t *testing.T) *http.Request { + // test request setup + var ( + req *http.Request + err error + ) + if testcase.nilBody { + req, err = http.NewRequest(testcase.method, testcase.url, nil) + } else { + req, err = http.NewRequest(testcase.method, testcase.url, strings.NewReader(testcase.body)) + } + if err != nil { + t.Fatal(err) + } + req = mux.SetURLVars(req, testcase.urlRouteVars) + + if testcase.headers != nil { + req.Header = *testcase.headers + } + if testcase.authorizeID != "" { + auth, err := ag.getAuthByID(testcase.authorizeID) + if err != nil { + t.Fatal(err) + } + // getAuthHeader requires a content type and body + req.Header.Set("Authorization", hawk.NewRequestAuth(req, + &hawk.Credentials{ + ID: auth.ID, + Key: auth.Key, + Hash: sha256.New}, + 0).RequestHeader()) + } + + return req +} + +func (testcase *HandlerTestCase) ValidateResponse(t *testing.T, w *httptest.ResponseRecorder) { + if w.Code != testcase.expectedStatus { + t.Fatalf("test case %s: got code %d but expected %d", + testcase.name, w.Code, testcase.expectedStatus) + } + if w.Body.String() != testcase.expectedBody { + t.Fatalf("test case %s: got body %q expected %q", testcase.name, w.Body.String(), testcase.expectedBody) + } + for expectedHeader, expectedHeaderVals := range testcase.expectedHeaders { + vals, ok := w.Header()[expectedHeader] + if !ok { + t.Fatalf("test case %s: expected header %q not found", testcase.name, expectedHeader) + } + if strings.Join(vals, "") != strings.Join(expectedHeaderVals, "") { + t.Fatalf("test case %s: header vals %q did not match expected %q ", testcase.name, vals, expectedHeaderVals) + } + } +} + +func (testcase *HandlerTestCase) Run(t *testing.T, handler func(http.ResponseWriter, *http.Request)) { + // test request setup + var req = testcase.NewRequest(t) + + // run the request + w := httptest.NewRecorder() + handler(w, req) + + // validate response + testcase.ValidateResponse(t, w) +} + func TestBadRequest(t *testing.T) { t.Parallel() @@ -268,38 +359,41 @@ func TestLBHeartbeat(t *testing.T) { } } -func checkHeartbeatReturnsExpectedStatusAndBody(t *testing.T, name, method string, expectedStatusCode int, expectedBody []byte) { - req, err := http.NewRequest(method, "http://foo.bar/__heartbeat__", nil) - if err != nil { - t.Fatal(err) - } - w := httptest.NewRecorder() - ag.handleHeartbeat(w, req) - if w.Code != expectedStatusCode { - t.Fatalf("test case %s failed with code %d but %d was expected", - name, w.Code, expectedStatusCode) - } - if !bytes.Equal(w.Body.Bytes(), expectedBody) { - t.Fatalf("test case %s returned unexpected heartbeat body %q expected %q", name, w.Body.Bytes(), expectedBody) - } -} - func TestHeartbeat(t *testing.T) { t.Parallel() - var TESTCASES = []struct { - name string - method string - expectedHTTPStatus int - expectedBody string - }{ - {"returns 200 for GET", `GET`, http.StatusOK, "{}"}, - {"returns 405 for POST", `POST`, http.StatusMethodNotAllowed, "POST method not allowed; endpoint accepts GET only\r\nrequest-id: -\n"}, - {"returns 405 for PUT", `PUT`, http.StatusMethodNotAllowed, "PUT method not allowed; endpoint accepts GET only\r\nrequest-id: -\n"}, - {"returns 405 for HEAD", `HEAD`, http.StatusMethodNotAllowed, "HEAD method not allowed; endpoint accepts GET only\r\nrequest-id: -\n"}, + var TESTCASES = []HandlerTestCase{ + { + name: "returns 200 for GET", + method: "GET", + url: "http://foo.bar/__heartbeat__", + expectedStatus: http.StatusOK, + expectedBody: "{}", + }, + { + name: "returns 405 for POST", + method: "POST", + url: "http://foo.bar/__heartbeat__", + expectedStatus: http.StatusMethodNotAllowed, + expectedBody: "POST method not allowed; endpoint accepts GET only\r\nrequest-id: -\n", + }, + { + name: "returns 405 for PUT", + method: "PUT", + url: "http://foo.bar/__heartbeat__", + expectedStatus: http.StatusMethodNotAllowed, + expectedBody: "PUT method not allowed; endpoint accepts GET only\r\nrequest-id: -\n", + }, + { + name: "returns 405 for HEAD", + method: "HEAD", + url: "http://foo.bar/__heartbeat__", + expectedStatus: http.StatusMethodNotAllowed, + expectedBody: "HEAD method not allowed; endpoint accepts GET only\r\nrequest-id: -\n", + }, } for _, testcase := range TESTCASES { - checkHeartbeatReturnsExpectedStatusAndBody(t, testcase.name, testcase.method, testcase.expectedHTTPStatus, []byte((testcase.expectedBody))) + testcase.Run(t, ag.handleHeartbeat) } } @@ -310,9 +404,14 @@ func TestHeartbeatChecksHSMStatusFails(t *testing.T) { hsmSignerConf: &ag.getSigners()[0].(*contentsignature.ContentSigner).Configuration, } - expectedStatus := http.StatusInternalServerError - expectedBody := []byte("{\"hsmAccessible\":false}") - checkHeartbeatReturnsExpectedStatusAndBody(t, "returns 500 for GET with HSM inaccessible", `GET`, expectedStatus, expectedBody) + var testcase = HandlerTestCase{ + name: "returns 500 for GET with HSM inaccessible", + method: "GET", + url: "http://foo.bar/__heartbeat__", + expectedStatus: http.StatusInternalServerError, + expectedBody: "{\"hsmAccessible\":false}", + } + testcase.Run(t, ag.handleHeartbeat) ag.heartbeatConf = nil } @@ -321,9 +420,14 @@ func TestHeartbeatChecksHSMStatusFailsWhenNotConfigured(t *testing.T) { // NB: do not run in parallel with TestHeartbeat* ag.heartbeatConf = nil - expectedStatus := http.StatusInternalServerError - expectedBody := []byte("Missing heartbeat config\r\nrequest-id: -\n") - checkHeartbeatReturnsExpectedStatusAndBody(t, "returns 500 for GET without heartbeat config HSM", `GET`, expectedStatus, expectedBody) + var testcase = HandlerTestCase{ + name: "returns 500 for GET without heartbeat config HSM", + method: "GET", + url: "http://foo.bar/__heartbeat__", + expectedStatus: http.StatusInternalServerError, + expectedBody: "Missing heartbeat config\r\nrequest-id: -\n", + } + testcase.Run(t, ag.handleHeartbeat) } func TestHeartbeatChecksDBStatusOKAndTimesout(t *testing.T) { @@ -343,25 +447,39 @@ func TestHeartbeatChecksDBStatusOKAndTimesout(t *testing.T) { DBCheckTimeout: 2 * time.Second, } - // check OK run locally requires running DB container - expectedStatus := http.StatusOK - expectedBody := []byte("{\"dbAccessible\":true}") - checkHeartbeatReturnsExpectedStatusAndBody(t, "returns 200 for GET with DB accessible", `GET`, expectedStatus, expectedBody) + var dbAccessibleTestCase = HandlerTestCase{ + name: "returns 200 for GET with DB accessible", + method: "GET", + url: "http://foo.bar/__heartbeat__", + expectedStatus: http.StatusOK, + expectedBody: "{\"dbAccessible\":true}", + } + dbAccessibleTestCase.Run(t, ag.handleHeartbeat) // drop timeout ag.heartbeatConf.DBCheckTimeout = 1 * time.Nanosecond // check DB request times out - expectedStatus = http.StatusOK - expectedBody = []byte("{\"dbAccessible\":false}") - checkHeartbeatReturnsExpectedStatusAndBody(t, "returns 200 for GET with DB time out", `GET`, expectedStatus, expectedBody) + var dbTimeoutTestCase = HandlerTestCase{ + name: "returns 200 for GET with DB time out", + method: "GET", + url: "http://foo.bar/__heartbeat__", + expectedStatus: http.StatusOK, + expectedBody: "{\"dbAccessible\":false}", + } + dbTimeoutTestCase.Run(t, ag.handleHeartbeat) // restore longer timeout and close the DB connection ag.heartbeatConf.DBCheckTimeout = 1 * time.Second db.Close() // check DB request still fails - expectedStatus = http.StatusOK - expectedBody = []byte("{\"dbAccessible\":false}") - checkHeartbeatReturnsExpectedStatusAndBody(t, "returns 200 for GET with DB inaccessible", `GET`, expectedStatus, expectedBody) + var dbOfflineTestCase = HandlerTestCase{ + name: "returns 200 for GET with DB inaccessible", + method: "GET", + url: "http://foo.bar/__heartbeat__", + expectedStatus: http.StatusOK, + expectedBody: "{\"dbAccessible\":false}", + } + dbOfflineTestCase.Run(t, ag.handleHeartbeat) ag.db = nil } @@ -575,28 +693,7 @@ func TestHandleGetAuthKeyIDs(t *testing.T) { 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 - url string - - // urlRouteVars are https://pkg.go.dev/github.com/gorilla/mux#Vars - // as configured with the handler at /auths/{auth_id:[a-zA-Z0-9-_]{1,255}}/keyids - // there should only be an auth_id var and it should match the url value - urlRouteVars map[string]string - - // headers are additional http headers to set - headers *http.Header - - // user/auth ID to build an Authorization header for - authorizeID string - nilBody bool - body string - - expectedStatus int - expectedHeaders http.Header - expectedBody string - }{ + var testcases = []HandlerTestCase{ { name: "invalid method POST returns 405", method: "POST", @@ -720,60 +817,174 @@ func TestHandleGetAuthKeyIDs(t *testing.T) { expectedHeaders: http.Header{"Content-Type": []string{"application/json"}}, }, } - for i, testcase := range testcases { - // test request setup - var ( - req *http.Request - err error - ) - if testcase.nilBody { - req, err = http.NewRequest(testcase.method, testcase.url, nil) - } else { - req, err = http.NewRequest(testcase.method, testcase.url, strings.NewReader(testcase.body)) - } - if err != nil { - t.Fatal(err) - } - req = mux.SetURLVars(req, testcase.urlRouteVars) + for _, testcase := range testcases { + testcase.Run(t, ag.handleGetAuthKeyIDs) + } +} - if testcase.headers != nil { - req.Header = *testcase.headers - } - if testcase.authorizeID != "" { - auth, err := ag.getAuthByID(testcase.authorizeID) - if err != nil { - t.Fatal(err) - } - // getAuthHeader requires a content type and body - req.Header.Set("Authorization", hawk.NewRequestAuth(req, - &hawk.Credentials{ - ID: auth.ID, - Key: auth.Key, - Hash: sha256.New}, - 0).RequestHeader()) - } +func TestHandleGetConfig(t *testing.T) { + t.Parallel() - // run the request - w := httptest.NewRecorder() - ag.handleGetAuthKeyIDs(w, req) + // A signer that alice can access + var autographDummyRsaConfig = signer.SanitizedConfig{ + ID: "dummyrsa", + Type: "genericrsa", + Mode: "pss", + PublicKey: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtEM/Vdfd4Vl9wmeVdCYuWYnQl0Zc9RW5hLE4hFA+c277qanE8XCK+ap/c5so87XngLLfacB3zZhGxIOut/4SlEBOAUmVNCfnTO+YkRk3A8OyJ4XNqdn+/ov78ZbssGf+0zws2BcwZYwhtuTvro3yi62FQ7T1TpT5VjljH7sHW/iZnS/RKiY4DwqAN799gkB+Gwovtroabh2w5OX0P+PYyUbJLFQeo5uiAQ8cAXTlHqCkj11GYgU4ttVDuFGotKRyaRn1F+yKxE4LQcAULx7s0KzvS35mNU+MoywLWjy9a4TcjK0nq+BjspKX4UkNwVstvH18hQWun7E+dxTi59cRmwIDAQAB", + Hash: "sha256", + PrivateKey: "a4087b340c1770b70cf5192413c6d7d7dbfc0a9db09b167965f133d672412c75", + } + autographDummyRsaJSON, _ := json.Marshal(autographDummyRsaConfig) - // validate response - if w.Code != testcase.expectedStatus { - t.Fatalf("test case %s (%d): got code %d but expected %d", - testcase.name, i, w.Code, testcase.expectedStatus) - } - if w.Body.String() != testcase.expectedBody { - t.Fatalf("test case %s (%d): got body %q expected %q", testcase.name, i, w.Body.String(), testcase.expectedBody) - } - for expectedHeader, expectedHeaderVals := range testcase.expectedHeaders { - vals, ok := w.Header()[expectedHeader] - if !ok { - t.Fatalf("test case %s (%d): expected header %q not found", testcase.name, i, expectedHeader) - } - if strings.Join(vals, "") != strings.Join(expectedHeaderVals, "") { - t.Fatalf("test case %s (%d): header vals %q did not match expected %q ", testcase.name, i, vals, expectedHeaderVals) - } - } + // A signer that both alice and bob can access + var autographAppKey2Config = signer.SanitizedConfig{ + ID: "appkey2", + Type: "contentsignature", + Mode: "p256ecdsa", + PublicKey: "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEMdzAsqkWQiP8Fo89qTleJcuEjBtp2c6z16sC7BAS5KXvUGghURYq3utZw8En6Ik/4Om8c7EW/+EO+EkHShhgdA==", + PrivateKey: "e41a1fb2466a1ba6c690ca6e3030c157745cfebe3c650f60c4c807ad781dd979", + } + autographAppKey2JSON, _ := json.Marshal(autographAppKey2Config) + + var testcases = []HandlerTestCase{ + { + name: "invalid method POST returns 405", + method: "POST", + url: "http://foo.bar/config/dummyrsa", + nilBody: true, + expectedStatus: http.StatusMethodNotAllowed, + expectedBody: "POST method not allowed; endpoint accepts GET only\r\nrequest-id: -\n", + expectedHeaders: http.Header{"Content-Type": []string{"text/plain; charset=utf-8"}}, + }, + { + name: "invalid method PUT returns 405", + method: "PUT", + url: "http://foo.bar/config/dummyrsa", + nilBody: true, + expectedStatus: http.StatusMethodNotAllowed, + expectedBody: "PUT method not allowed; endpoint accepts GET only\r\nrequest-id: -\n", + expectedHeaders: http.Header{"Content-Type": []string{"text/plain; charset=utf-8"}}, + }, + { + name: "invalid method OPTIONS returns 405", + method: "OPTIONS", + url: "http://foo.bar/config/dummyrsa", + nilBody: true, + expectedStatus: http.StatusMethodNotAllowed, + expectedBody: "OPTIONS method not allowed; endpoint accepts GET only\r\nrequest-id: -\n", + expectedHeaders: http.Header{"Content-Type": []string{"text/plain; charset=utf-8"}}, + }, + { + name: "invalid method HEAD returns 405", + method: "HEAD", + url: "http://foo.bar/config/dummyrsa", + nilBody: true, + expectedStatus: http.StatusMethodNotAllowed, + expectedBody: "HEAD method not allowed; endpoint accepts GET only\r\nrequest-id: -\n", + expectedHeaders: http.Header{"Content-Type": []string{"text/plain; charset=utf-8"}}, + }, + { + name: "GET with empty body returns 200", + method: "GET", + url: "http://foo.bar/config/dummyrsa", + urlRouteVars: map[string]string{"keyid": "dummyrsa"}, + nilBody: false, + body: "", + authorizeID: "alice", + expectedStatus: http.StatusOK, + expectedBody: string(autographDummyRsaJSON), + expectedHeaders: http.Header{"Content-Type": []string{"application/json"}}, + }, + { + name: "GET with non-empty body returns 400", + method: "GET", + url: "http://foo.bar/config/dummyrsa", + urlRouteVars: map[string]string{"keyid": "dummyrsa"}, + nilBody: false, + body: "foobar/---", + expectedStatus: http.StatusBadRequest, + expectedBody: "endpoint received unexpected request body\r\nrequest-id: -\n", + expectedHeaders: http.Header{"Content-Type": []string{"text/plain; charset=utf-8"}}, + }, + { + name: "GET with misconfigured keyid route param returns 500", + method: "GET", + url: "http://foo.bar/config/dummyrsa", + urlRouteVars: map[string]string{}, // missing keyid + nilBody: true, + expectedStatus: http.StatusInternalServerError, + expectedBody: "route is improperly configured\r\nrequest-id: -\n", + expectedHeaders: http.Header{"Content-Type": []string{"text/plain; charset=utf-8"}}, + }, + { + name: "GET missing Authorization header returns 401", + method: "GET", + url: "http://foo.bar/config/dummyrsa", + urlRouteVars: map[string]string{"keyid": "dummyrsa"}, + nilBody: true, + expectedStatus: http.StatusUnauthorized, + expectedBody: "authorization verification failed: missing Authorization header\r\nrequest-id: -\n", + expectedHeaders: http.Header{"Content-Type": []string{"text/plain; charset=utf-8"}}, + }, + { + name: "GET with invalid Authorization header returns 401", + method: "GET", + url: "http://foo.bar/config/dummyrsa", + urlRouteVars: map[string]string{"keyid": "dummyrsa"}, + headers: &http.Header{"Authorization": []string{`Hawk id="dh37fgj492je", ts="1353832234", nonce="j4h3g2", ext="some-app-ext-data", mac="6R4rV5iE+NPoym+WwjeHzjAGXUtLNIxmo1vpMofpLAE="`}}, + nilBody: true, + expectedStatus: http.StatusUnauthorized, + expectedBody: "authorization verification failed: hawk: credential error with id dh37fgj492je and app : unknown id\r\nrequest-id: -\n", + expectedHeaders: http.Header{"Content-Type": []string{"text/plain; charset=utf-8"}}, + }, + { + name: "GET with invalid auth id url param returns 400", + method: "GET", + url: "http://foo.bar/config/", + urlRouteVars: map[string]string{"keyid": ""}, + nilBody: true, + expectedStatus: http.StatusBadRequest, + expectedBody: "keyid in URL path '' is invalid, it must match ^[a-zA-Z0-9-_]{1,64}$\r\nrequest-id: -\n", + expectedHeaders: http.Header{"Content-Type": []string{"text/plain; charset=utf-8"}}, + }, + { + name: "GET with auth returns 404 for unknown keyid", + method: "GET", + url: "http://foo.bar/config/unknown", + urlRouteVars: map[string]string{"keyid": "unknown"}, + nilBody: true, + authorizeID: "alice", + expectedStatus: http.StatusNotFound, + expectedBody: "keyid unknown was not found for user alice\r\nrequest-id: -\n", + expectedHeaders: http.Header{"Content-Type": []string{"text/plain; charset=utf-8"}}, + }, + { + name: "GET with auth returns 404 for mismatched auth ids (bob cannot get alice's keyids)", + method: "GET", + url: "http://foo.bar/config/dummyrsa", + urlRouteVars: map[string]string{"keyid": "dummyrsa"}, + nilBody: true, + authorizeID: "bob", + expectedStatus: http.StatusNotFound, + expectedBody: "keyid dummyrsa was not found for user bob\r\nrequest-id: -\n", + expectedHeaders: http.Header{"Content-Type": []string{"text/plain; charset=utf-8"}}, + }, + { + name: "GET with empty body returns 200 (bob can fetch their own keyids)", + method: "GET", + url: "http://foo.bar/config/appkey2", + urlRouteVars: map[string]string{"keyid": "appkey2"}, + nilBody: false, + body: "", + authorizeID: "bob", + expectedStatus: http.StatusOK, + expectedBody: string(autographAppKey2JSON), + expectedHeaders: http.Header{"Content-Type": []string{"application/json"}}, + }, + } + + for _, testcase := range testcases { + testcase.Run(t, ag.handleGetConfig) } } diff --git a/main.go b/main.go index 2a899afa9..882deb7cf 100755 --- a/main.go +++ b/main.go @@ -15,7 +15,6 @@ import ( "net/http" "os" "os/signal" - "regexp" "strings" "syscall" "time" @@ -214,6 +213,7 @@ func run(conf configuration, listen string, debug bool) { router.HandleFunc("/sign/data", ag.handleSignature).Methods("POST") router.HandleFunc("/sign/hash", ag.handleSignature).Methods("POST") router.HandleFunc("/auths/{auth_id:[a-zA-Z0-9-_]{1,255}}/keyids", ag.handleGetAuthKeyIDs).Methods("GET") + router.HandleFunc("/config/{keyid:[a-zA-Z0-9-_]{1,64}}", ag.handleGetConfig).Methods("GET") if os.Getenv("AUTOGRAPH_PROFILE") == "1" { err = setRuntimeConfig() if err != nil { @@ -388,7 +388,7 @@ func (a *autographer) initHSM(conf configuration) { func (a *autographer) addSigners(signerConfs []signer.Configuration) error { sids := make(map[string]bool) for _, signerConf := range signerConfs { - if !regexp.MustCompile(signer.IDFormat).MatchString(signerConf.ID) { + if !signer.IDFormatRegexp.MatchString(signerConf.ID) { return fmt.Errorf("signer ID %q does not match the permitted format %q", signerConf.ID, signer.IDFormat) } diff --git a/signer/genericrsa/rsa.go b/signer/genericrsa/rsa.go index c69004acc..6dd65b71b 100644 --- a/signer/genericrsa/rsa.go +++ b/signer/genericrsa/rsa.go @@ -121,6 +121,7 @@ func New(conf signer.Configuration) (s *RSASigner, err error) { default: return nil, fmt.Errorf("genericrsa: unsupported hash %q for signer %q, must be 'sha1' or 'sha256'", s.Hash, s.ID) } + s.Hash = conf.Hash s.SaltLength = conf.SaltLength switch s.Mode { @@ -149,6 +150,7 @@ func (s *RSASigner) Config() signer.Configuration { PrivateKey: s.PrivateKey, PublicKey: s.PublicKey, SignerOpts: s.sigOpts, + Hash: s.Hash, } } diff --git a/signer/gpg2/gpg2.go b/signer/gpg2/gpg2.go index c9f8c0371..cebc96fbd 100644 --- a/signer/gpg2/gpg2.go +++ b/signer/gpg2/gpg2.go @@ -278,6 +278,7 @@ func (s *GPG2Signer) Config() signer.Configuration { PrivateKey: s.PrivateKey, PublicKey: s.PublicKey, Mode: s.Mode, + KeyID: s.KeyID, } } diff --git a/signer/signer.go b/signer/signer.go index d57a607c0..1f45eecb5 100644 --- a/signer/signer.go +++ b/signer/signer.go @@ -12,10 +12,13 @@ import ( "crypto/elliptic" "crypto/rand" "crypto/rsa" + "crypto/sha1" + "crypto/sha256" "crypto/x509" "encoding/base64" "encoding/pem" "fmt" + "hash" "io" "regexp" "strings" @@ -34,6 +37,9 @@ import ( // IDFormat is a regex for the format IDs must follow const IDFormat = `^[a-zA-Z0-9-_]{1,64}$` +// IDFormatRegexp is the compiled regex from IDFormat +var IDFormatRegexp = regexp.MustCompile(IDFormat) + // RSACacheConfig is a config for the RSAKeyCache type RSACacheConfig struct { // NumKeys is the number of RSA keys matching the issuer size @@ -159,6 +165,92 @@ type Configuration struct { hsmCtx *pkcs11.Ctx } +// SanitizedConfig allows the export of public signer configuration +type SanitizedConfig struct { + // These fields are copied verbatim from signer.Configuration + ID string `json:"id" yaml:"id"` + Type string `json:"type" yaml:"type"` + Mode string `json:"mode" yaml:"mode"` + PublicKey string `json:"publickey,omitempty" yaml:"publickey,omitempty"` + IssuerCert string `json:"issuercert,omitempty" yaml:"issuercert,omitempty"` + Certificate string `json:"certificate,omitempty" yaml:"certificate,omitempty"` + X5U string `json:"x5u,omitempty" yaml:"x5u,omitempty"` + KeyID string `json:"keyid,omitempty" yaml:"keyid,omitempty"` + SubdomainOverride string `json:"subdomain_override,omitempty" yaml:"subdomainoverride,omitempty"` + Validity time.Duration `json:"validity,omitempty" yaml:"validity,omitempty"` + ClockSkewTolerance time.Duration `json:"clock_skew_tolerance,omitempty" yaml:"clockskewtolerance,omitempty"` + ChainUploadLocation string `json:"chain_upload_location,omitempty" yaml:"chainuploadlocation,omitempty"` + CaCert string `json:"cacert,omitempty" yaml:"cacert,omitempty"` + Hash string `json:"hash,omitempty" yaml:"hash,omitempty"` + SaltLength int `json:"saltlength,omitempty" yaml:"saltlength,omitempty"` + + // Hash digests of the private keys. + PrivateKey string `json:"privatekey,omitempty" yaml:"privatekey,omitempty"` + IssuerPrivKey string `json:"issuerprivkey,omitempty" yaml:"issuerprivkey,omitempty"` + + // If a certificate is present, add fingerprints and expiration dates. + CertFingerprintSha1 string `json:"cert_sha1,omitempty" yaml:"cert_sha1,omitempty"` + CertFingerprintSha256 string `json:"cert_sha256,omitempty" yaml:"cert_sha256,omitempty"` + CertDateStart string `json:"cert_date_start,omitempty" yaml:"cert_date_start,omitempty"` + CertDateEnd string `json:"cert_date_end,omitempty" yaml:"cert_date_end,omitempty"` +} + +func hashFingerprint(secret []byte, algorithm hash.Hash) string { + // Empty strings should stay empty + if len(secret) == 0 { + return "" + } + + algorithm.Write(secret) + return fmt.Sprintf("%x", algorithm.Sum(nil)) +} + +// Sanitize configuration to make it suitable for public export +func (cfg *Configuration) Sanitize() *SanitizedConfig { + result := &SanitizedConfig{ + // Copy public values verbatim. + ID: cfg.ID, + Type: cfg.Type, + Mode: cfg.Mode, + PublicKey: cfg.PublicKey, + IssuerCert: cfg.IssuerCert, + Certificate: cfg.Certificate, + X5U: cfg.X5U, + KeyID: cfg.KeyID, + SubdomainOverride: cfg.SubdomainOverride, + Validity: cfg.Validity, + ClockSkewTolerance: cfg.ClockSkewTolerance, + ChainUploadLocation: cfg.ChainUploadLocation, + CaCert: cfg.CaCert, + Hash: cfg.Hash, + SaltLength: cfg.SaltLength, + + // Hash private keys, if present. + PrivateKey: hashFingerprint([]byte(cfg.PrivateKey), sha256.New()), + IssuerPrivKey: hashFingerprint([]byte(cfg.IssuerPrivKey), sha256.New()), + } + + // If a certificate exists - parse it. + certDER, _ := pem.Decode([]byte(cfg.Certificate)) + if certDER != nil && certDER.Type == "CERTIFICATE" { + certX509, err := x509.ParseCertificate(certDER.Bytes) + if err == nil { + result.CertFingerprintSha1 = hashFingerprint(certDER.Bytes, sha1.New()) + result.CertFingerprintSha256 = hashFingerprint(certDER.Bytes, sha256.New()) + start, err := certX509.NotBefore.MarshalText() + if err == nil { + result.CertDateStart = string(start) + } + end, err := certX509.NotAfter.MarshalText() + if err == nil { + result.CertDateEnd = string(end) + } + } + } + + return result +} + // InitHSM indicates that an HSM has been initialized func (cfg *Configuration) InitHSM(ctx *pkcs11.Ctx) { cfg.isHsmAvailable = true diff --git a/signer/signer_test.go b/signer/signer_test.go index d451e68dc..6c66af06d 100644 --- a/signer/signer_test.go +++ b/signer/signer_test.go @@ -8,6 +8,7 @@ package signer import ( "crypto/rsa" + "encoding/json" "fmt" "testing" ) @@ -306,3 +307,116 @@ var rejectedFileNames = []string{ "control*chars", "_non_alpha_start", } + +func TestSanitizedConfig(t *testing.T) { + for _, testcase := range sanitizerTestCases { + r := testcase.cfg.Sanitize() + if testcase.result != *r { + expect, _ := json.MarshalIndent(testcase.result, "", " ") + actual, _ := json.MarshalIndent(r, "", " ") + t.Logf("expected: %s", expect) + t.Logf("actual: %s", actual) + t.Fatalf("Sanitized config failed for: %s", testcase.cfg.ID) + } + } +} + +var sanitizerTestCases = []struct { + cfg Configuration + result SanitizedConfig +}{ + // All the fields are copied over. + {cfg: Configuration{ + ID: "copy-public-values", + Type: "bar", + Mode: "qux", + PublicKey: "This is a public key", + IssuerCert: "This is an issuer", + Certificate: "This is a certificate", + X5U: "http://example.com/", + KeyID: "This is a keyid", + SubdomainOverride: "allizom.com", + Validity: 12345, + ClockSkewTolerance: 555, + ChainUploadLocation: "file:///somewhere/over/the/rainbow", + CaCert: "The root of evil", + Hash: "sha666", + SaltLength: 9001, + }, + result: SanitizedConfig{ + ID: "copy-public-values", + Type: "bar", + Mode: "qux", + PublicKey: "This is a public key", + IssuerCert: "This is an issuer", + Certificate: "This is a certificate", + X5U: "http://example.com/", + KeyID: "This is a keyid", + SubdomainOverride: "allizom.com", + Validity: 12345, + ClockSkewTolerance: 555, + ChainUploadLocation: "file:///somewhere/over/the/rainbow", + CaCert: "The root of evil", + Hash: "sha666", + SaltLength: 9001, + }}, + // Private keys are hashed with SHA256 + {cfg: Configuration{ + ID: "private-key-example", + PrivateKey: "hello world", + IssuerPrivKey: "Lorem Ipsum", + }, + result: SanitizedConfig{ + ID: "private-key-example", + // echo -n "hello world" | sha256sum + PrivateKey: "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", + // echo -n "Lorem Ipsum" | sha256sum + IssuerPrivKey: "030dc1f936c3415aff3f3357163515190d347a28e758e1f717d17bae453541c9", + }}, + // Certificates should parse out the fingerprint and validity dates. + {cfg: Configuration{ + ID: "cert-extra-data", + Certificate: ` +-----BEGIN CERTIFICATE----- +MIICXDCCAeKgAwIBAgIIFYW6xg9HrnAwCgYIKoZIzj0EAwMwXzELMAkGA1UEBhMC +VVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRAwDgYDVQQK +EwdNb3ppbGxhMRkwFwYDVQQDExBjc3Jvb3QxNTUwODUxMDA2MB4XDTE4MTIyMTE1 +NTY0NloXDTI5MDIyMjE1NTY0NlowYDELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNB +MRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRAwDgYDVQQKEwdNb3ppbGxhMRowGAYD +VQQDExFjc2ludGVyMTU1MDg1MTAwNjB2MBAGByqGSM49AgEGBSuBBAAiA2IABAwF +9wOPiv/1oBdxSyOO6fe8KkFJCiyRx2KIXhsT4BwWY8AGHoCfBNm/Swdg+OSi+TdH +dF+5eUrKiqG4PvdWoGGS4rtHqY3ayeF9GRaaLpLMdZkhc/MVJygJoecmsXM2O6Nq +MGgwDgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUFBwMDMA8GA1UdEwEB +/wQFMAMBAf8wMAYDVR0eAQH/BCYwJKAiMCCCHi5jb250ZW50LXNpZ25hdHVyZS5t +b3ppbGxhLm9yZzAKBggqhkjOPQQDAwNoADBlAjBss+GLdMdLT2Y/g73OE9x0WyUG +vqzO7klt20yytmhaYMIPT/zRnWsHZbqEijHMzGsCMQDEoKetuWkyBkzAytS6l+ss +mYigBlwySY+gTqsjuIrydWlKaOv1GU+PXbwX0cQuaN8= +-----END CERTIFICATE-----`, + }, + result: SanitizedConfig{ + ID: "cert-extra-data", + Certificate: ` +-----BEGIN CERTIFICATE----- +MIICXDCCAeKgAwIBAgIIFYW6xg9HrnAwCgYIKoZIzj0EAwMwXzELMAkGA1UEBhMC +VVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRAwDgYDVQQK +EwdNb3ppbGxhMRkwFwYDVQQDExBjc3Jvb3QxNTUwODUxMDA2MB4XDTE4MTIyMTE1 +NTY0NloXDTI5MDIyMjE1NTY0NlowYDELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNB +MRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRAwDgYDVQQKEwdNb3ppbGxhMRowGAYD +VQQDExFjc2ludGVyMTU1MDg1MTAwNjB2MBAGByqGSM49AgEGBSuBBAAiA2IABAwF +9wOPiv/1oBdxSyOO6fe8KkFJCiyRx2KIXhsT4BwWY8AGHoCfBNm/Swdg+OSi+TdH +dF+5eUrKiqG4PvdWoGGS4rtHqY3ayeF9GRaaLpLMdZkhc/MVJygJoecmsXM2O6Nq +MGgwDgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUFBwMDMA8GA1UdEwEB +/wQFMAMBAf8wMAYDVR0eAQH/BCYwJKAiMCCCHi5jb250ZW50LXNpZ25hdHVyZS5t +b3ppbGxhLm9yZzAKBggqhkjOPQQDAwNoADBlAjBss+GLdMdLT2Y/g73OE9x0WyUG +vqzO7klt20yytmhaYMIPT/zRnWsHZbqEijHMzGsCMQDEoKetuWkyBkzAytS6l+ss +mYigBlwySY+gTqsjuIrydWlKaOv1GU+PXbwX0cQuaN8= +-----END CERTIFICATE-----`, + // openssl x509 -outform DER | shasum + CertFingerprintSha1: "793a92cb335c3846ffed7f8c112137cd8a75e7c7", + // openssl x509 -outform DER | sha256sum + CertFingerprintSha256: "61bd2500b732d2889a1b17c24365741550534fb715cd4f7c463a23a35bd931ee", + // openssl x509 -noout -text + CertDateStart: "2018-12-21T15:56:46Z", + CertDateEnd: "2029-02-22T15:56:46Z", + }}, +} diff --git a/tools/autograph-client/client.go b/tools/autograph-client/client.go index 4f75d1206..7c20e01a8 100644 --- a/tools/autograph-client/client.go +++ b/tools/autograph-client/client.go @@ -67,13 +67,13 @@ func urlToRequestType(url string) requestType { func main() { var ( - 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 + userid, pass, data, hash, url, infile, outfile, outkeyfile, outFilesPrefix, keyid, cn, pk7digest, rootPath, verificationTimeInput, listConfig 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") @@ -145,7 +145,7 @@ examples: 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.StringVar(&url, "t", `http://localhost:8000`, "target server URL") flag.IntVar(&iter, "i", 1, "number of signatures to request") flag.IntVar(&maxworkers, "m", 1, "maximum number of parallel workers") flag.StringVar(&cn, "cn", "", "when signing XPI, sets the CN to the add-on ID") @@ -156,10 +156,14 @@ examples: flag.StringVar(&verificationTimeInput, "vt", "", "Time to verify XPI signatures at in RFC3339 format. Defaults to at client invokation + 1 minute to account for time to transfer and sign the XPI") flag.BoolVar(&noVerify, "noverify", false, "Skip verifying successful responses. Default false.") flag.BoolVar(&listKeyIDs, "listkeyids", false, "List key IDs for the signer") + flag.StringVar(&listConfig, "listconfig", "", "List signer config") flag.BoolVar(&debug, "D", false, "debug logs: show raw requests & responses") flag.Parse() + // Strip trailing slashes from the URL, if any. + url = strings.TrimRight(url, "/") + if verificationTimeInput == "" { verificationTime = time.Now().UTC().Add(time.Minute) if debug { @@ -178,6 +182,11 @@ examples: os.Exit(0) } + if listConfig != "" { + listSignerConfig(cli, debug, url, userid, pass, listConfig) + os.Exit(0) + } + var ( inputFiles []formats.SigningFile request formats.SignatureRequest @@ -498,6 +507,50 @@ func listKeyIDsForCurrentUser(cli *http.Client, debug bool, url, userid, pass st fmt.Println(string(indentedJSON)) } +func listSignerConfig(cli *http.Client, debug bool, url, userid, pass string, keyid string) { + url = url + "/config/" + keyid + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + log.Fatal(err) + } + req.Header.Set("Authorization", hawk.NewRequestAuth(req, + &hawk.Credentials{ + ID: userid, + Key: pass, + Hash: sha256.New}, + 0).RequestHeader()) + if debug { + fmt.Printf("DEBUG: sending request\nDEBUG: %+v\n", req) + } + resp, err := cli.Do(req) + if err != nil || resp == nil { + log.Fatal(err) + } + if debug { + fmt.Printf("DEBUG: received response\nDEBUG: %+v\n", resp) + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + log.Fatal(err) + } + if debug { + fmt.Printf("DEBUG: %s\n", body) + } + if resp.StatusCode != http.StatusOK { + log.Fatalf("%s %s", resp.Status, body) + } + + // pretty print output + var indentedJSON bytes.Buffer + err = json.Indent(&indentedJSON, body, "", " ") + if err != nil { + log.Fatalf("error unmarshaling JSON %q", err) + } + fmt.Println(indentedJSON.String()) +} + // parseVerificationTime parses an RFC3339 timestamp or exits func parseVerificationTime(rfc3339Timestamp string) time.Time { parsed, err := time.Parse(time.RFC3339, rfc3339Timestamp) diff --git a/tools/autograph-monitor/Dockerfile.lambda-emulator b/tools/autograph-monitor/Dockerfile.lambda-emulator index 29a96f399..840ce22be 100644 --- a/tools/autograph-monitor/Dockerfile.lambda-emulator +++ b/tools/autograph-monitor/Dockerfile.lambda-emulator @@ -4,9 +4,12 @@ USER root RUN cp /app/src/autograph/bin/test_monitor.sh /usr/local/bin/test_monitor.sh RUN curl -Lo /usr/local/bin/aws-lambda-rie \ - https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/latest/download/aws-lambda-rie \ - && \ - chmod +x /usr/local/bin/aws-lambda-rie /usr/local/bin/test_monitor.sh + https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/latest/download/aws-lambda-rie && \ + chmod +x /usr/local/bin/aws-lambda-rie + +# Use an entrypoint to determine the AUTOGRAPH_ROOT_HASH +COPY lambda-setup-entrypoint.sh /usr/local/bin/lambda-setup-entrypoint.sh +ENTRYPOINT ["/usr/local/bin/lambda-setup-entrypoint.sh"] USER app -CMD ["/usr/local/bin/aws-lambda-rie", "/go/bin/autograph-monitor"] +CMD ["/go/bin/autograph-monitor"] diff --git a/tools/autograph-monitor/lambda-setup-entrypoint.sh b/tools/autograph-monitor/lambda-setup-entrypoint.sh new file mode 100755 index 000000000..5b3555e46 --- /dev/null +++ b/tools/autograph-monitor/lambda-setup-entrypoint.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +set -e +set -o pipefail + +# Wait for the heartbeat +HEARTBEAT=$(curl --silent \ + --connect-timeout 5 \ + --max-time 10 \ + --retry-connrefused \ + --retry 5 \ + --retry-delay 5 \ + --retry-max-time 60 \ + "${AUTOGRAPH_URL}/__heartbeat__") +RETCODE=$? +if [ $RETCODE -ne 0 ]; then + echo "Failed to reach autograph heartbeat" >&2 + exit $RETCODE +fi + +# Fetch the normandy root hash +export AUTOGRAPH_ROOT_HASH=$(autograph-client -t "$AUTOGRAPH_URL" -listconfig normandy | \ + jq -r '.cacert' | openssl x509 -outform der | openssl dgst -sha256 -hex | \ + awk '{print $2}' | tr '[:lower:]' '[:upper:]') + +echo "Autograph instance: $AUTOGRAPH_URL" +echo "Got Root hash: $AUTOGRAPH_ROOT_HASH" +echo "Starting lambda: $@" +/usr/local/bin/aws-lambda-rie "$@" diff --git a/tools/softhsm/Dockerfile b/tools/softhsm/Dockerfile index 9175ce596..adc63f78b 100644 --- a/tools/softhsm/Dockerfile +++ b/tools/softhsm/Dockerfile @@ -48,9 +48,6 @@ RUN cd /app/src/autograph/tools/genpki/ && \ python3 configurator.py -c /app/autograph.softhsm.yaml -i -s kinto \ -p issuercert -v "$(grep 'inter cert path' /app/genpki.out | awk '{print $4}')" && \ python3 configurator.py -c /app/autograph.softhsm.yaml -i -s kinto \ - -p cacert -v "$(grep 'root cert path' /app/genpki.out | awk '{print $4}')" && \ - cp /app/autograph.softhsm.yaml /tmp/ && \ - /bin/bash /app/src/autograph/tools/softhsm/hash_signer_cacert.sh /app/autograph.softhsm.yaml normandy > /tmp/normandy_dev_root_hash.txt && \ - cat /tmp/normandy_dev_root_hash.txt + -p cacert -v "$(grep 'root cert path' /app/genpki.out | awk '{print $4}')" CMD /go/bin/autograph -c /app/autograph.softhsm.yaml