├── .env.example ├── .github └── workflows │ └── continuous_integration.yaml ├── .gitignore ├── .golangci.yml ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── db.go ├── db └── db.sql ├── docker-compose.yml.example ├── go.mod ├── go.sum ├── main.go ├── server.go ├── server_test.go └── tcn ├── parse.go ├── parse_test.go ├── report.go ├── signedreport.go └── tcn.go /.env.example: -------------------------------------------------------------------------------- 1 | POSTGRES_DB= 2 | POSTGRES_USER= 3 | POSTGRES_PASSWORD= 4 | POSTGRES_HOST=postgres 5 | -------------------------------------------------------------------------------- /.github/workflows/continuous_integration.yaml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | build: 9 | name: Build and Test 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Set up Go 1.x 14 | uses: actions/setup-go@v2 15 | with: 16 | go-version: ^1.13 17 | id: go 18 | 19 | - name: Check out code into the Go module directory 20 | uses: actions/checkout@v2 21 | 22 | - name: Spin up a detatched Postgress image 23 | run: docker run --detach --name postgres -p 5432:5432 -v $(pwd)/db:/data -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres postgres 24 | 25 | - name: Get dependencies 26 | run: go get ./... 27 | 28 | - name: Build 29 | run: go build . 30 | 31 | - name: Initialize database 32 | run: docker exec postgres psql -f /data/db.sql -U postgres 33 | 34 | - name: Run Tests 35 | run: POSTGRES_USER=postgres POSTGRES_PASSWORD=postgres go test -v ./... 36 | 37 | - name: Run docker build 38 | run: docker build . 39 | 40 | security-scanning: 41 | name: Security Scanning 42 | runs-on: ubuntu-latest 43 | 44 | steps: 45 | - uses: actions/checkout@v2 46 | 47 | - name: Run Gosec Security Scanner 48 | uses: securego/gosec@master 49 | with: 50 | args: ./... 51 | 52 | linting: 53 | name: Linting 54 | runs-on: ubuntu-latest 55 | 56 | steps: 57 | - uses: actions/checkout@v2 58 | 59 | - name: Linting 60 | run: docker run --rm -v $(pwd):/app -w /app golangci/golangci-lint:v1.24.0 golangci-lint run -v 61 | 62 | publishing: 63 | name: Publishing 64 | if: startsWith(github.ref, 'refs/tags/') 65 | needs: 66 | - build 67 | - security-scanning 68 | - linting 69 | runs-on: ubuntu-latest 70 | 71 | steps: 72 | - name: Check out code 73 | uses: actions/checkout@v2 74 | 75 | - name: Push docker image 76 | uses: docker/build-push-action@v1 77 | with: 78 | username: ${{ github.actor }} 79 | password: ${{ secrets.GITHUB_TOKEN }} 80 | registry: docker.pkg.github.com 81 | repository: ${{ github.repository }}/api-backend 82 | tag_with_ref: true 83 | tags: latest 84 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE 2 | /.idea 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | 13 | # Unit test / coverage reports 14 | htmlcov/ 15 | .tox/ 16 | .nox/ 17 | .coverage 18 | .coverage.* 19 | .cache 20 | nosetests.xml 21 | coverage.xml 22 | *.cover 23 | *.py,cover 24 | .hypothesis/ 25 | .pytest_cache/ 26 | 27 | # Translations 28 | *.mo 29 | *.pot 30 | 31 | # Django stuff: 32 | *.log 33 | local_settings.py 34 | db.sqlite3 35 | db.sqlite3-journal 36 | 37 | # Flask stuff: 38 | instance/ 39 | .webassets-cache 40 | 41 | # Scrapy stuff: 42 | .scrapy 43 | 44 | # Sphinx documentation 45 | docs/_build/ 46 | 47 | # PyBuilder 48 | target/ 49 | 50 | # Jupyter Notebook 51 | .ipynb_checkpoints 52 | 53 | # IPython 54 | profile_default/ 55 | ipython_config.py 56 | 57 | # pyenv 58 | .python-version 59 | 60 | # pipenv 61 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 62 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 63 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 64 | # install all needed dependencies. 65 | #Pipfile.lock 66 | 67 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 68 | __pypackages__/ 69 | 70 | # Celery stuff 71 | celerybeat-schedule 72 | celerybeat.pid 73 | 74 | # SageMath parsed files 75 | *.sage.py 76 | 77 | # Environments 78 | .env 79 | .venv 80 | env/ 81 | venv/ 82 | ENV/ 83 | env.bak/ 84 | venv.bak/ 85 | 86 | # Spyder project settings 87 | .spyderproject 88 | .spyproject 89 | 90 | # Rope project settings 91 | .ropeproject 92 | 93 | # mkdocs documentation 94 | /site 95 | 96 | # mypy 97 | .mypy_cache/ 98 | .dmypy.json 99 | dmypy.json 100 | 101 | # Pyre type checker 102 | .pyre/ 103 | 104 | config.py -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | disable: 3 | - gosimple -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | We accept pull requests. 2 | 3 | - Please make sure you understand the security concepts behind this app. 4 | - Please make sure your code passes CI before creating your PR. 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1-alpine as builder 2 | 3 | RUN apk update && apk add --no-cache git ca-certificates tzdata && update-ca-certificates 4 | 5 | ENV USER=ito 6 | ENV UID=10001 7 | 8 | RUN adduser \ 9 | --disabled-password \ 10 | --gecos "" \ 11 | --home "/nonexistent" \ 12 | --shell "/sbin/nologin" \ 13 | --no-create-home \ 14 | --uid "${UID}" \ 15 | "${USER}" 16 | WORKDIR $GOPATH/src/ito/api-backend/ 17 | COPY . . 18 | 19 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /go/bin/backend 20 | 21 | FROM scratch 22 | 23 | COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo 24 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 25 | COPY --from=builder /etc/passwd /etc/passwd 26 | COPY --from=builder /etc/group /etc/group 27 | COPY --from=builder /go/bin/backend /go/bin/backend 28 | 29 | USER ${USER}:${USER} 30 | 31 | ENTRYPOINT ["/go/bin/backend"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The Clear BSD License 2 | 3 | Copyright (c) 2020, ito 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS 21 | LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 25 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ito - Backend API 2 | 3 | API for supplying and verifying TCNs of users confirmed infected 4 | 5 | ![Tests](https://github.com/ito-org/api-backend/workflows/Continuous%20Integration/badge.svg) 6 | [![Docs](https://img.shields.io/website?label=documentation&url=https%3A%2F%2Fdocs.ito-app.org%2Fapi-backend)](https://docs.ito-app.org/api-backend) 7 | [![License](https://img.shields.io/badge/license-BSD--3--Clause--Clear-blue)](LICENSE) 8 | 9 | ## Prerequisites 10 | 11 | - Go 12 | - PostgreSQL 13 | 14 | ## Run it 15 | 16 | Run the backend directly by spinning up a [Postgres Docker](https://hub.docker.com/_/postgres/) container and running `go run github.com/ito-org/api-backend`. Alternative, you can spin up the backend in combination with the database via docker-compose. Run `docker-compose build && docker-compose up -d`. 17 | 18 | **IMPORTANT**: Keep in mind that you need to set the environment variables as shown below. 19 | 20 | ## Environment variables 21 | 22 | You can supply database credentials through environment variables. The following are available: 23 | 24 | * `POSTGRES_DB` 25 | * `POSTGRES_USER` 26 | * `POSTGRES_PASSWORD` 27 | 28 | You can either set them directly when running the application or set them through an `.env` file in the project root. For docker-compose, the `.env` file is required. 29 | -------------------------------------------------------------------------------- /db.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/ito-org/go-backend/tcn" 7 | "github.com/jmoiron/sqlx" 8 | _ "github.com/lib/pq" 9 | ) 10 | 11 | // NewDBConnection creates and tests a new db connection and returns it. 12 | func NewDBConnection(dbHost, dbUser, dbPassword, dbName string) (*DBConnection, error) { 13 | connStr := fmt.Sprintf( 14 | "host=%s user=%s password=%s dbname=%s sslmode=disable", 15 | dbHost, 16 | dbUser, 17 | dbPassword, 18 | dbName, 19 | ) 20 | 21 | db, err := sqlx.Connect("postgres", connStr) 22 | if err != nil { 23 | fmt.Printf("Failed to connect to Postgres database: %s\n", err.Error()) 24 | return nil, err 25 | } 26 | return &DBConnection{db}, err 27 | } 28 | 29 | // DBConnection implements several functions for fetching and manipulation 30 | // of reports in the database. 31 | type DBConnection struct { 32 | *sqlx.DB 33 | } 34 | 35 | func (db *DBConnection) insertMemo(memo *tcn.Memo) (uint64, error) { 36 | var newID uint64 37 | if err := db.QueryRowx( 38 | ` 39 | INSERT INTO 40 | Memo(mtype, mlen, mdata) 41 | VALUES($1, $2, $3) 42 | RETURNING id; 43 | `, 44 | memo.Type, 45 | memo.Len, 46 | memo.Data[:], 47 | ).Scan(&newID); err != nil { 48 | fmt.Printf("Failed to insert memo into database: %s\n", err.Error()) 49 | return 0, err 50 | } 51 | return newID, nil 52 | } 53 | 54 | func (db *DBConnection) insertReport(report *tcn.Report) (uint64, error) { 55 | memoID, err := db.insertMemo(report.Memo) 56 | if err != nil { 57 | return 0, err 58 | } 59 | 60 | var newID uint64 61 | 62 | if err = db.QueryRowx( 63 | ` 64 | INSERT INTO 65 | Report(rvk, tck_bytes, j_1, j_2, memo_id) 66 | VALUES($1, $2, $3, $4, $5) 67 | RETURNING id; 68 | `, 69 | report.RVK, 70 | report.TCKBytes[:], 71 | report.J1, 72 | report.J2, 73 | memoID, 74 | ).Scan(&newID); err != nil { 75 | fmt.Printf("Failed to insert report into database: %s\n", err.Error()) 76 | return 0, err 77 | } 78 | return newID, nil 79 | } 80 | 81 | func (db *DBConnection) insertSignedReport(signedReport *tcn.SignedReport) error { 82 | reportID, err := db.insertReport(signedReport.Report) 83 | if err != nil { 84 | return err 85 | } 86 | 87 | _, err = db.Exec( 88 | ` 89 | INSERT INTO 90 | SignedReport(report_id, sig) 91 | VALUES($1, $2) 92 | `, 93 | reportID, 94 | signedReport.Sig[:], 95 | ) 96 | if err != nil { 97 | fmt.Printf("Failed to insert signed report into database: %s\n", err.Error()) 98 | return err 99 | } 100 | return nil 101 | } 102 | 103 | func (db *DBConnection) scanSignedReports(rows *sqlx.Rows) ([]*tcn.SignedReport, error) { 104 | signedReports := []*tcn.SignedReport{} 105 | for rows.Next() { 106 | signedReport := &tcn.SignedReport{ 107 | Report: &tcn.Report{ 108 | TCKBytes: [32]uint8{}, 109 | Memo: &tcn.Memo{}, 110 | }, 111 | Sig: []byte{}, 112 | } 113 | tckBytesDest := []byte{} 114 | if err := rows.Scan( 115 | &signedReport.Report.RVK, 116 | &tckBytesDest, 117 | &signedReport.Report.J1, 118 | &signedReport.Report.J2, 119 | &signedReport.Report.Memo.Type, 120 | &signedReport.Report.Memo.Len, 121 | &signedReport.Report.Memo.Data, 122 | &signedReport.Sig, 123 | ); err != nil { 124 | fmt.Printf("Failed to scan signed report: %s\n", err.Error()) 125 | return nil, err 126 | } 127 | 128 | copy(signedReport.Report.TCKBytes[:], tckBytesDest[:32]) 129 | signedReports = append(signedReports, signedReport) 130 | } 131 | return signedReports, nil 132 | } 133 | 134 | func (db *DBConnection) getSignedReports() ([]*tcn.SignedReport, error) { 135 | rows, err := db.Queryx( 136 | ` 137 | SELECT r.rvk, r.tck_bytes, r.j_1, r.j_2, m.mtype, m.mlen, m.mdata, sr.sig 138 | FROM SignedReport sr 139 | JOIN Report r ON sr.report_id = r.id 140 | JOIN Memo m ON r.memo_id = m.id; 141 | `, 142 | ) 143 | if err != nil { 144 | fmt.Printf("Failed to get signed reports from database: %s\n", err.Error()) 145 | return nil, err 146 | } 147 | defer rows.Close() 148 | signedReports, err := db.scanSignedReports(rows) 149 | if err != nil { 150 | return nil, err 151 | } 152 | return signedReports, nil 153 | } 154 | 155 | // getNewSignedReports returns all signed reports that were made after lastReport. 156 | func (db *DBConnection) getNewSignedReports(lastReport *tcn.Report) ([]*tcn.SignedReport, error) { 157 | rows, err := db.Queryx( 158 | ` 159 | SELECT r.rvk, r.tck_bytes, r.j_1, r.j_2, m.mtype, m.mlen, m.mdata, sr.sig 160 | FROM SignedReport sr 161 | JOIN Report r ON sr.report_id = r.id 162 | JOIN Memo m ON r.memo_id = m.id 163 | WHERE r.timestamp > ( 164 | SELECT MIN(r2.timestamp) 165 | FROM Report r2 166 | WHERE r2.rvk = $1 167 | AND r2.tck_bytes = $2 168 | AND r2.j_1 = $3 169 | AND r2.j_2 = $4 170 | ); 171 | `, 172 | lastReport.RVK, 173 | lastReport.TCKBytes[:], 174 | lastReport.J1, 175 | lastReport.J2, 176 | ) 177 | if err != nil { 178 | fmt.Printf("Failed to get signed reports from database: %s\n", err.Error()) 179 | return nil, err 180 | } 181 | defer rows.Close() 182 | signedReports, err := db.scanSignedReports(rows) 183 | if err != nil { 184 | return nil, err 185 | } 186 | return signedReports, nil 187 | } 188 | -------------------------------------------------------------------------------- /db/db.sql: -------------------------------------------------------------------------------- 1 | CREATE DOMAIN uint8 AS smallint 2 | CHECK(VALUE >= 0 AND VALUE < 256); 3 | 4 | CREATE TABLE IF NOT EXISTS Memo ( 5 | id bigserial primary key, 6 | mtype uint8 not null, 7 | mlen uint8 not null, 8 | mdata bytea 9 | ); 10 | 11 | CREATE TABLE IF NOT EXISTS Report ( 12 | id bigserial primary key, 13 | rvk bytea not null, 14 | tck_bytes bytea not null, 15 | j_1 uint8 not null, 16 | j_2 uint8 not null, 17 | memo_id bigserial not null references Memo(id), 18 | timestamp timestamp default current_timestamp 19 | ); 20 | 21 | CREATE TABLE IF NOT EXISTS SignedReport ( 22 | id bigserial primary key, 23 | report_id bigserial not null references Report(id), 24 | sig bytea not null 25 | ); -------------------------------------------------------------------------------- /docker-compose.yml.example: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | go-backend: 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | ports: 9 | - 8080:8080 10 | environment: 11 | POSTGRES_PASSWORD: "ito" 12 | POSTGRES_USER: "postgres" 13 | POSTGRES_DB: "postgres" 14 | POSTGRES_HOST: "postgres" 15 | depends_on: 16 | - postgres 17 | networks: 18 | - itonet 19 | restart: always 20 | 21 | postgres: 22 | image: postgres:12 23 | environment: 24 | POSTGRES_PASSWORD: "ito" 25 | POSTGRES_USER: "postgres" 26 | POSTGRES_DB: "postgres" 27 | POSTGRES_HOST: "postgres" 28 | ports: 29 | - 5432:5432 30 | volumes: 31 | - ./db/db.sql:/docker-entrypoint-initdb.d/db.sql 32 | - dbvol:/var/lib/postgresql/data 33 | networks: 34 | - itonet 35 | restart: always 36 | environment: 37 | POSTGRES_PASSWORD: "ito" 38 | POSTGRES_USER: "postgres" 39 | POSTGRES_DB: "postgres" 40 | 41 | networks: 42 | itonet: 43 | 44 | volumes: 45 | dbvol: 46 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ito-org/go-backend 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect 7 | github.com/gin-gonic/gin v1.6.2 8 | github.com/golang/protobuf v1.4.0 // indirect 9 | github.com/jmoiron/sqlx v1.2.0 10 | github.com/lib/pq v1.4.0 11 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 12 | github.com/modern-go/reflect2 v1.0.1 // indirect 13 | github.com/stretchr/testify v1.5.1 14 | github.com/urfave/cli v1.22.4 15 | golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= 3 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 4 | github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= 5 | github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 6 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 8 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 10 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 11 | github.com/gin-gonic/gin v1.6.2 h1:88crIK23zO6TqlQBt+f9FrPJNKm9ZEr7qjp9vl/d5TM= 12 | github.com/gin-gonic/gin v1.6.2/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= 13 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 14 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= 15 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 16 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= 17 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 18 | github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= 19 | github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= 20 | github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= 21 | github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= 22 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 23 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 24 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 25 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 26 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 27 | github.com/golang/protobuf v1.4.0 h1:oOuy+ugB+P/kBdUnG5QaMXSIyJ1q38wWSojYCb3z5VQ= 28 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 29 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 30 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 31 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 32 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 33 | github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA= 34 | github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= 35 | github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= 36 | github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= 37 | github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= 38 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 39 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= 40 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 41 | github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 42 | github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU= 43 | github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 44 | github.com/lib/pq v1.4.0 h1:TmtCFbH+Aw0AixwyttznSMQDgbR5Yed/Gg6S8Funrhc= 45 | github.com/lib/pq v1.4.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 46 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 47 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 48 | github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= 49 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 50 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 51 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 52 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 53 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= 54 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 55 | github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= 56 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 57 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 58 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 59 | github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= 60 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 61 | github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= 62 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 63 | github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= 64 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 65 | github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= 66 | github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 67 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 68 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 69 | github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= 70 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 71 | github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= 72 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 73 | github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= 74 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 75 | github.com/urfave/cli v1.22.4 h1:u7tSpNPPswAFymm8IehJhy4uJMlUuU/GmqSkvJ1InXA= 76 | github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= 77 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg= 78 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 79 | golang.org/x/sys v0.0.0-20200413165638-669c56c373c4 h1:opSr2sbRXk5X5/givKrrKj9HXxFpW2sdCiP8MJSKLQY= 80 | golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 81 | golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f h1:gWF768j/LaZugp8dyS4UwsslYCYz9XgFxvlgsn0n9H8= 82 | golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 83 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 84 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 85 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 86 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 87 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 88 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 89 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 90 | google.golang.org/protobuf v1.21.0 h1:qdOKuR/EIArgaWNjetjgTzgVTAZ+S/WXVrq9HW9zimw= 91 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 92 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 93 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 94 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 95 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 96 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/urfave/cli" 8 | ) 9 | 10 | func readPostgresSettings() (dbHost, dbName, dbUser, dbPassword string) { 11 | dbHost = os.Getenv("POSTGRES_HOST") 12 | dbName = os.Getenv("POSTGRES_DB") 13 | dbUser = os.Getenv("POSTGRES_USER") 14 | dbPassword = os.Getenv("POSTGRES_PASSWORD") 15 | 16 | if dbHost == "" { 17 | dbHost = "localhost" 18 | } 19 | if dbName == "" { 20 | dbName = "postgres" 21 | } 22 | if dbUser == "" { 23 | dbUser = "postgres" 24 | } 25 | if dbPassword == "" { 26 | dbPassword = "ito" 27 | } 28 | 29 | return 30 | } 31 | 32 | func main() { 33 | var port string 34 | 35 | app := &cli.App{ 36 | Flags: []cli.Flag{ 37 | &cli.StringFlag{ 38 | Name: "port", 39 | Value: "8080", 40 | Usage: "Port for the server to run on", 41 | Destination: &port, 42 | }, 43 | }, 44 | Action: func(ctx *cli.Context) error { 45 | dbHost, dbName, dbUser, dbPassword := readPostgresSettings() 46 | dbConnection, err := NewDBConnection(dbHost, dbUser, dbPassword, dbName) 47 | if err != nil { 48 | return err 49 | } 50 | return GetRouter(port, dbConnection).Run(fmt.Sprintf(":%s", port)) 51 | }, 52 | } 53 | 54 | if err := app.Run(os.Args); err != nil { 55 | fmt.Println(err.Error()) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/hex" 5 | "io/ioutil" 6 | "net/http" 7 | 8 | "github.com/gin-gonic/gin" 9 | "github.com/ito-org/go-backend/tcn" 10 | ) 11 | 12 | const ( 13 | requestBodyReadError = "Failed to read request body" 14 | invalidRequestError = "Invalid request" 15 | reportVerificationError = "Failed to verify report" 16 | ) 17 | 18 | // GetRouter returns the Gin router. 19 | func GetRouter(port string, dbConnection *DBConnection) *gin.Engine { 20 | h := &TCNReportHandler{ 21 | dbConn: dbConnection, 22 | } 23 | 24 | r := gin.Default() 25 | r.POST("/tcnreport", h.postTCNReport) 26 | r.GET("/tcnreport", h.getTCNReport) 27 | return r 28 | } 29 | 30 | // TCNReportHandler implements the handler functions for the API endpoints. 31 | // It also holds the database connection that's used by the handler functions. 32 | type TCNReportHandler struct { 33 | dbConn *DBConnection 34 | } 35 | 36 | func (h *TCNReportHandler) postTCNReport(c *gin.Context) { 37 | body := c.Request.Body 38 | data, err := ioutil.ReadAll(body) 39 | if err != nil { 40 | c.String(http.StatusBadRequest, requestBodyReadError) 41 | return 42 | } 43 | 44 | signedReport, err := tcn.GetSignedReport(data) 45 | if err != nil { 46 | c.String(http.StatusBadRequest, err.Error()) 47 | return 48 | } 49 | 50 | // If the memo field doesn't exist or the memo type is not ito's code, we 51 | // simply ignore the request. 52 | if signedReport.Report.Memo == nil || signedReport.Report.Memo.Type != 0x2 { 53 | c.String(http.StatusBadRequest, invalidRequestError) 54 | return 55 | } 56 | 57 | ok, err := signedReport.Verify() 58 | if err != nil { 59 | c.String(http.StatusBadRequest, err.Error()) 60 | return 61 | } 62 | 63 | if !ok { 64 | c.String(http.StatusBadRequest, reportVerificationError) 65 | return 66 | } 67 | 68 | if err := h.dbConn.insertSignedReport(signedReport); err != nil { 69 | c.String(http.StatusInternalServerError, err.Error()) 70 | return 71 | } 72 | 73 | c.Status(http.StatusOK) 74 | } 75 | 76 | func (h *TCNReportHandler) getTCNReport(c *gin.Context) { 77 | var signedReports []*tcn.SignedReport 78 | var err error 79 | 80 | // The 'from' query param is used to only get reports that were made after 81 | // the one in 'from'. 82 | from := c.Query("from") 83 | 84 | if from == "" { 85 | signedReports, err = h.dbConn.getSignedReports() 86 | } else { 87 | fromBytes, err := hex.DecodeString(from) 88 | if err != nil { 89 | c.String(http.StatusBadRequest, err.Error()) 90 | return 91 | } 92 | 93 | var report *tcn.Report 94 | report, err = tcn.GetReport(fromBytes) 95 | if err != nil { 96 | c.String(http.StatusBadRequest, err.Error()) 97 | return 98 | } 99 | signedReports, err = h.dbConn.getNewSignedReports(report) 100 | if err != nil { 101 | c.String(http.StatusInternalServerError, err.Error()) 102 | return 103 | } 104 | } 105 | if err != nil { 106 | c.String(http.StatusInternalServerError, err.Error()) 107 | return 108 | } 109 | 110 | data := []byte{} 111 | for _, sr := range signedReports { 112 | b, err := sr.Bytes() 113 | if err != nil { 114 | c.String(http.StatusInternalServerError, err.Error()) 115 | return 116 | } 117 | data = append(data, b...) 118 | } 119 | 120 | c.Data(http.StatusOK, "application/octet-stream", data) 121 | } 122 | -------------------------------------------------------------------------------- /server_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "crypto" 6 | "encoding/hex" 7 | "fmt" 8 | "io/ioutil" 9 | "net/http" 10 | "net/http/httptest" 11 | "os" 12 | "reflect" 13 | "testing" 14 | 15 | "github.com/gin-gonic/gin" 16 | "github.com/ito-org/go-backend/tcn" 17 | "github.com/stretchr/testify/assert" 18 | ) 19 | 20 | var handler *TCNReportHandler 21 | 22 | // Init function before every test 23 | func TestMain(m *testing.M) { 24 | // Initialize the database connection and the handler structure so we can 25 | // call the handler functions directly instead of making actual HTTP 26 | // requests. This allows us to create and the database connection which 27 | // would otherwise not happen. 28 | 29 | dbHost, dbName, dbUser, dbPassword := readPostgresSettings() 30 | 31 | dbConn, err := NewDBConnection(dbHost, dbUser, dbPassword, dbName) 32 | if err != nil { 33 | panic(err.Error()) 34 | } 35 | 36 | handler = &TCNReportHandler{ 37 | dbConn: dbConn, 38 | } 39 | code := m.Run() 40 | os.Exit(code) 41 | } 42 | 43 | func getGetRequest() (*httptest.ResponseRecorder, *http.Request) { 44 | return getGetRequestWithParam("") 45 | } 46 | 47 | func getGetRequestWithParam(from string) (*httptest.ResponseRecorder, *http.Request) { 48 | path := "/tcnreport" 49 | if from != "" { 50 | path += fmt.Sprintf("?from=%s", from) 51 | } 52 | 53 | rec := httptest.NewRecorder() 54 | req, _ := http.NewRequest("GET", path, nil) 55 | return rec, req 56 | } 57 | 58 | func getPostRequest(data []byte) (*httptest.ResponseRecorder, *http.Request) { 59 | rec := httptest.NewRecorder() 60 | req, _ := http.NewRequest("POST", "/tcnreport", bytes.NewReader(data)) 61 | return rec, req 62 | } 63 | 64 | func TestPostTCNReport(t *testing.T) { 65 | _, rak, report, err := tcn.GenerateReport(0, 1, []byte("symptom data")) 66 | if err != nil { 67 | t.Error(err) 68 | return 69 | } 70 | 71 | signedReport, err := tcn.GenerateSignedReport(rak, report) 72 | if err != nil { 73 | t.Error(err) 74 | return 75 | } 76 | 77 | b, err := signedReport.Bytes() 78 | if err != nil { 79 | t.Error(err) 80 | return 81 | } 82 | 83 | rec, req := getPostRequest(b) 84 | ctx, _ := gin.CreateTestContext(rec) 85 | ctx.Request = req 86 | handler.postTCNReport(ctx) 87 | 88 | assert.Equal(t, http.StatusOK, rec.Code) 89 | } 90 | 91 | func TestPostTCNReportInvalidSig(t *testing.T) { 92 | // Store just the report here since we're going to sign it with a different 93 | // key 94 | _, _, report, err := tcn.GenerateReport(0, 1, nil) 95 | if err != nil { 96 | t.Error(err) 97 | } 98 | 99 | // Generate second private key to sign with so we can force an error to 100 | // happen 101 | _, rak2, _, err := tcn.GenerateReport(0, 1, nil) 102 | if err != nil { 103 | t.Error(err) 104 | return 105 | } 106 | 107 | rb, err := report.Bytes() 108 | if err != nil { 109 | t.Error(err) 110 | return 111 | } 112 | 113 | fakeSignedReport, err := tcn.GenerateSignedReport(rak2, report) // Note: wrong key used here 114 | if err != nil { 115 | t.Error(err) 116 | return 117 | } 118 | 119 | // Manually concatenate the byte array that's sent to the server 120 | b := []byte{} 121 | b = append(b, rb...) 122 | b = append(b, fakeSignedReport.Sig...) 123 | 124 | rec, req := getPostRequest(b) 125 | ctx, _ := gin.CreateTestContext(rec) 126 | ctx.Request = req 127 | handler.postTCNReport(ctx) 128 | 129 | assert.Equal(t, http.StatusBadRequest, rec.Code) 130 | 131 | defer rec.Result().Body.Close() 132 | respData, err := ioutil.ReadAll(rec.Result().Body) 133 | if err != nil { 134 | t.Error(err) 135 | return 136 | } 137 | assert.Equal(t, reportVerificationError, string(respData)) 138 | } 139 | 140 | func TestPostTCNInvalidType(t *testing.T) { 141 | _, rak, report, err := tcn.GenerateReport(0, 1, nil) 142 | if err != nil { 143 | t.Error(err) 144 | return 145 | } 146 | 147 | // Not ito memo type 148 | report.Memo.Type = 0x1 149 | 150 | signedReport, err := tcn.GenerateSignedReport(rak, report) 151 | if err != nil { 152 | t.Error(err) 153 | return 154 | } 155 | 156 | b, err := signedReport.Bytes() 157 | if err != nil { 158 | t.Error(err) 159 | return 160 | } 161 | 162 | rec, req := getPostRequest(b) 163 | ctx, _ := gin.CreateTestContext(rec) 164 | ctx.Request = req 165 | handler.postTCNReport(ctx) 166 | 167 | assert.Equal(t, http.StatusBadRequest, rec.Code) 168 | } 169 | 170 | func TestPostTCNInvalidLength(t *testing.T) { 171 | _, rak, report, err := tcn.GenerateReport(0, 1, nil) 172 | if err != nil { 173 | t.Error(err) 174 | return 175 | } 176 | 177 | report.Memo.Len = 0 178 | report.Memo.Data = nil 179 | 180 | rb, err := report.Bytes() 181 | if err != nil { 182 | t.Error(err) 183 | return 184 | } 185 | 186 | rb = rb[1:] // This is where the report gets its invalid length 187 | 188 | sig, err := rak.Sign(nil, rb, crypto.Hash(0)) 189 | if err != nil { 190 | t.Error(err) 191 | return 192 | } 193 | 194 | signedReport := &tcn.SignedReport{ 195 | Report: report, 196 | Sig: sig, 197 | } 198 | 199 | var b []byte 200 | b = append(b, rb...) 201 | b = append(b, signedReport.Sig...) 202 | 203 | rec, req := getPostRequest(b) 204 | ctx, _ := gin.CreateTestContext(rec) 205 | ctx.Request = req 206 | handler.postTCNReport(ctx) 207 | 208 | assert.Equal(t, http.StatusBadRequest, rec.Code) 209 | } 210 | 211 | func TestGetTCNReports(t *testing.T) { 212 | signedReports := [5]*tcn.SignedReport{} 213 | for i := 0; i < 5; i++ { 214 | _, rak, report, _ := tcn.GenerateReport(0, 1, []byte("symptom data")) 215 | signedReport, err := tcn.GenerateSignedReport(rak, report) 216 | if err != nil { 217 | t.Error(err.Error()) 218 | return 219 | } 220 | 221 | signedReportBytes, err := signedReport.Bytes() 222 | if err != nil { 223 | t.Error(err.Error()) 224 | return 225 | } 226 | 227 | // POST reports 228 | rec, req := getPostRequest(signedReportBytes) 229 | ctx, _ := gin.CreateTestContext(rec) 230 | ctx.Request = req 231 | handler.postTCNReport(ctx) 232 | 233 | signedReports[i] = signedReport 234 | } 235 | 236 | // GET reports 237 | rec, req := getGetRequest() 238 | ctx, _ := gin.CreateTestContext(rec) 239 | ctx.Request = req 240 | handler.getTCNReport(ctx) 241 | defer rec.Result().Body.Close() 242 | 243 | body, err := ioutil.ReadAll(rec.Result().Body) 244 | if err != nil { 245 | t.Error(err.Error()) 246 | } 247 | 248 | if len(body) == 0 { 249 | t.Error("Body is empty") 250 | return 251 | } 252 | 253 | // Retrieve the signed reports from the handler function's response 254 | retSignedReports := tcn.GetSignedReports(body) 255 | if err != nil { 256 | t.Error(err.Error()) 257 | return 258 | } 259 | 260 | found := 0 261 | for _, r := range signedReports { 262 | for _, rr := range retSignedReports { 263 | if reflect.DeepEqual(r, rr) { 264 | found++ 265 | } 266 | } 267 | } 268 | 269 | assert.Equal(t, len(signedReports), found) 270 | } 271 | 272 | func postSignedReports(signedReportBytes []byte) { 273 | rec, req := getPostRequest(signedReportBytes) 274 | ctx, _ := gin.CreateTestContext(rec) 275 | ctx.Request = req 276 | handler.postTCNReport(ctx) 277 | } 278 | 279 | func TestGetNewTCNReports(t *testing.T) { 280 | signedReports := [5]*tcn.SignedReport{} 281 | for i := 0; i < 5; i++ { 282 | _, rak, report, _ := tcn.GenerateReport(0, 1, []byte("symptom data")) 283 | signedReport, err := tcn.GenerateSignedReport(rak, report) 284 | if err != nil { 285 | t.Error(err.Error()) 286 | return 287 | } 288 | 289 | signedReportBytes, err := signedReport.Bytes() 290 | if err != nil { 291 | t.Error(err.Error()) 292 | return 293 | } 294 | 295 | postSignedReports(signedReportBytes) 296 | if i == 2 { 297 | // Post it twice 298 | postSignedReports(signedReportBytes) 299 | } 300 | signedReports[i] = signedReport 301 | } 302 | 303 | // Prepare the hex values for reports 2 and 3 304 | // 2 is the one that was inserted twice and 3 was insert once. 305 | sr2Bytes, _ := signedReports[2].Bytes() 306 | hex2Dst := make([]byte, hex.EncodedLen(len(sr2Bytes))) 307 | hex.Encode(hex2Dst, sr2Bytes) 308 | 309 | // GET reports after second report (second report was inserted twice so 310 | // the returned reports should contain the copy of that report that was 311 | // inserted later, but also the rest of the reports) 312 | rec, req := getGetRequestWithParam(string(hex2Dst)) 313 | ctx, _ := gin.CreateTestContext(rec) 314 | ctx.Request = req 315 | handler.getTCNReport(ctx) 316 | defer rec.Result().Body.Close() 317 | 318 | body, err := ioutil.ReadAll(rec.Result().Body) 319 | if err != nil { 320 | t.Error(err.Error()) 321 | } 322 | 323 | if len(body) == 0 { 324 | t.Error("Body is empty") 325 | return 326 | } 327 | 328 | // Retrieve the signed reports from the handler function's response 329 | retSignedReports := tcn.GetSignedReports(body) 330 | if err != nil { 331 | t.Error(err.Error()) 332 | return 333 | } 334 | 335 | found := 0 336 | for _, r := range signedReports { 337 | for _, rr := range retSignedReports { 338 | if reflect.DeepEqual(r, rr) { 339 | found++ 340 | } 341 | } 342 | } 343 | 344 | assert.Equal(t, len(signedReports[2:]), found) 345 | } 346 | -------------------------------------------------------------------------------- /tcn/parse.go: -------------------------------------------------------------------------------- 1 | package tcn 2 | 3 | import ( 4 | "crypto/ed25519" 5 | "encoding/binary" 6 | "errors" 7 | ) 8 | 9 | // GetSignedReport interprets data as a signed report and returns it as a 10 | // parsed structure. 11 | func GetSignedReport(data []byte) (*SignedReport, error) { 12 | if len(data) < SignedReportMinLength { 13 | return nil, errors.New("Data too short to be a valid signed report") 14 | } 15 | 16 | signedReport, _ := getSignedReport(data) 17 | return signedReport, nil 18 | } 19 | 20 | // getSignedReport returns the signed report contained in data and returns it 21 | // in combination with its length (end position), which allows for parsing of 22 | // multiple signed reports. 23 | func getSignedReport(data []byte) (*SignedReport, uint16) { 24 | report, reportEndPos := getReport(data) 25 | endPos := reportEndPos + ed25519.SignatureSize 26 | sig := data[reportEndPos:endPos] 27 | return &SignedReport{ 28 | Report: report, 29 | Sig: sig, 30 | }, endPos 31 | } 32 | 33 | // GetSignedReports gets all signed reports contained in a byte array and 34 | // returns them. 35 | func GetSignedReports(data []byte) []*SignedReport { 36 | signedReports := []*SignedReport{} 37 | var startPos uint16 38 | for { 39 | signedReport, endPos := getSignedReport(data[startPos:]) 40 | startPos += endPos 41 | signedReports = append(signedReports, signedReport) 42 | if int(startPos) >= len(data) { 43 | // TODO: Fail if the startPos is greater than the length of data 44 | // because this can only happen if something was wrong with the 45 | // data (e.g. memo length field incorrect) 46 | break 47 | } 48 | } 49 | return signedReports 50 | } 51 | 52 | // GetReport inteprets data as a report and returns it as a parsed structure. 53 | func GetReport(data []byte) (*Report, error) { 54 | if len(data) < ReportMinLength { 55 | return nil, errors.New("Data too short to be a valid signed report") 56 | } 57 | report, _ := getReport(data) 58 | return report, nil 59 | } 60 | 61 | // getReport is the internal function for getting reports from byte arrays. 62 | // It returns the report contained in the data field and also returns the 63 | // length / end position of the array. 64 | func getReport(data []byte) (report *Report, endPos uint16) { 65 | tckBytes := [32]byte{} 66 | copy(tckBytes[:], data[32:64]) 67 | 68 | memoDataLen := uint8(data[69]) 69 | 70 | memo := &Memo{ 71 | Type: data[68], 72 | Len: memoDataLen, 73 | Data: data[70 : 70+memoDataLen], 74 | } 75 | 76 | // TODO: do some array bounds checking 77 | 78 | report = &Report{ 79 | RVK: ed25519.PublicKey(data[:32]), 80 | TCKBytes: tckBytes, 81 | J1: binary.LittleEndian.Uint16(data[64:66]), 82 | J2: binary.LittleEndian.Uint16(data[66:68]), 83 | Memo: memo, 84 | } 85 | 86 | endPos = 70 + uint16(memoDataLen) 87 | 88 | return report, endPos 89 | } 90 | 91 | // GetReports gets all reports contained in a byte array and returns them. 92 | func GetReports(data []byte) []*Report { 93 | reports := []*Report{} 94 | var startPos uint16 95 | for { 96 | report, endPos := getReport(data[startPos:]) 97 | startPos += endPos 98 | reports = append(reports, report) 99 | if int(startPos) >= len(data) { 100 | // TODO: Fail if the startPos is greater than the length of data 101 | // because this can only happen if something was wrong with the 102 | // data (e.g. memo length field incorrect) 103 | break 104 | } 105 | } 106 | return reports 107 | } 108 | -------------------------------------------------------------------------------- /tcn/parse_test.go: -------------------------------------------------------------------------------- 1 | package tcn_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/ito-org/go-backend/tcn" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestGetReports(t *testing.T) { 11 | reports := [5]*tcn.Report{} 12 | for i := 0; i < 5; i++ { 13 | _, _, report, _ := tcn.GenerateReport(0, 1, []byte("symptom data")) 14 | reports[i] = report 15 | } 16 | 17 | reportBytes := []byte{} 18 | for _, r := range reports { 19 | b, err := r.Bytes() 20 | if err != nil { 21 | t.Error(err.Error()) 22 | return 23 | } 24 | reportBytes = append(reportBytes, b...) 25 | } 26 | 27 | retReports := tcn.GetReports(reportBytes) 28 | 29 | assert.Len(t, retReports, len(reports)) 30 | for i, rr := range retReports { 31 | assert.EqualValues(t, reports[i], rr) 32 | } 33 | } 34 | 35 | func TestGetReport(t *testing.T) { 36 | _, _, report, err := tcn.GenerateReport(0, 1, []byte("symptom data")) 37 | if err != nil { 38 | t.Error(err.Error()) 39 | return 40 | } 41 | 42 | rb, err := report.Bytes() 43 | if err != nil { 44 | t.Error(err.Error()) 45 | return 46 | } 47 | 48 | retReport, err := tcn.GetReport(rb) 49 | assert.NoError(t, err) 50 | assert.EqualValues(t, report, retReport) 51 | } 52 | 53 | func TestGetSignedReport(t *testing.T) { 54 | _, rak, report, err := tcn.GenerateReport(0, 4, []byte("sympton data")) 55 | if err != nil { 56 | t.Error(err.Error()) 57 | return 58 | } 59 | signedReport, err := tcn.GenerateSignedReport(rak, report) 60 | if err != nil { 61 | t.Error(err.Error()) 62 | return 63 | } 64 | 65 | srb, err := signedReport.Bytes() 66 | if err != nil { 67 | t.Error(err.Error()) 68 | return 69 | } 70 | 71 | retSignedReport, err := tcn.GetSignedReport(srb) 72 | 73 | assert.NoError(t, err) 74 | assert.EqualValues(t, signedReport, retSignedReport) 75 | } 76 | -------------------------------------------------------------------------------- /tcn/report.go: -------------------------------------------------------------------------------- 1 | package tcn 2 | 3 | import ( 4 | "crypto/ed25519" 5 | "crypto/sha256" 6 | "encoding/binary" 7 | "errors" 8 | "fmt" 9 | ) 10 | 11 | const ( 12 | // ITOMemoCode is the code that marks a report as an ito report in the 13 | // memo. 14 | ITOMemoCode = 0x2 15 | // ReportMinLength is the minimum length of a TCN report (with memo data 16 | // of length 0) in bytes. 17 | ReportMinLength = 70 18 | ) 19 | 20 | // Report represents a report as described in the TCN protocol: 21 | // https://github.com/TCNCoalition/TCN#reporting 22 | type Report struct { 23 | RVK ed25519.PublicKey `db:"rvk"` 24 | TCKBytes [32]byte `db:"tck_bytes"` 25 | J1 uint16 `db:"j_1"` 26 | J2 uint16 `db:"j_2"` 27 | *Memo 28 | } 29 | 30 | // Memo represents a memo data set as described in the TCN protocol: 31 | // https://github.com/TCNCoalition/TCN#reporting 32 | type Memo struct { 33 | Type uint8 `db:"mtype"` 34 | Len uint8 `db:"mlen"` 35 | Data []uint8 `db:"mdata"` 36 | } 37 | 38 | // Bytes converts r to a concatenated byte array represention. 39 | func (r *Report) Bytes() ([]byte, error) { 40 | var data []byte 41 | data = append(data, r.RVK...) 42 | data = append(data, r.TCKBytes[:]...) 43 | 44 | j1Bytes := make([]byte, 2) 45 | binary.LittleEndian.PutUint16(j1Bytes, r.J1) 46 | j2Bytes := make([]byte, 2) 47 | binary.LittleEndian.PutUint16(j2Bytes, r.J2) 48 | data = append(data, j1Bytes...) 49 | data = append(data, j2Bytes...) 50 | 51 | if r.Memo == nil { 52 | return nil, errors.New("Failed to create byte representation of report: memo field is null") 53 | } 54 | 55 | // Memo 56 | data = append(data, r.Memo.Type) 57 | data = append(data, r.Memo.Len) 58 | data = append(data, r.Memo.Data...) 59 | 60 | return data, nil 61 | } 62 | 63 | // GenerateMemo returns a memo instance with the given content. 64 | func GenerateMemo(content []byte) (*Memo, error) { 65 | if len(content) > 255 { 66 | return nil, errors.New("Data field contains too many bytes") 67 | } 68 | 69 | var c []byte 70 | // If content is nil, we don't want the data field in the memo to be nil 71 | // but empty instead. 72 | if content != nil { 73 | c = content 74 | } else { 75 | c = []byte{} 76 | } 77 | 78 | return &Memo{ 79 | Type: ITOMemoCode, 80 | Len: uint8(len(content)), 81 | Data: c, 82 | }, nil 83 | } 84 | 85 | // GenerateReport creates a public key, private key, and report according to TCN. 86 | func GenerateReport(j1, j2 uint16, memoData []byte) (*ed25519.PublicKey, *ed25519.PrivateKey, *Report, error) { 87 | rvk, rak, err := ed25519.GenerateKey(nil) 88 | if err != nil { 89 | return nil, nil, nil, err 90 | } 91 | 92 | tck0Hash := sha256.New() 93 | if _, err := tck0Hash.Write([]byte(HTCKDomainSep)); err != nil { 94 | fmt.Printf("Failed to write tck domain separator: %s\n", err.Error()) 95 | return nil, nil, nil, err 96 | } 97 | if _, err := tck0Hash.Write(rak); err != nil { 98 | fmt.Printf("Failed to write rak: %s\n", err.Error()) 99 | return nil, nil, nil, err 100 | } 101 | 102 | tck0Bytes := [32]byte{} 103 | copy(tck0Bytes[:32], tck0Hash.Sum(nil)) 104 | 105 | tck0 := &TemporaryContactKey{ 106 | Index: 0, 107 | RVK: rvk, 108 | TCKBytes: tck0Bytes, 109 | } 110 | 111 | tck1, err := tck0.Ratchet() 112 | if err != nil { 113 | return nil, nil, nil, err 114 | } 115 | 116 | memo, err := GenerateMemo(memoData) 117 | if err != nil { 118 | return nil, nil, nil, err 119 | } 120 | 121 | report := &Report{ 122 | RVK: rvk, 123 | TCKBytes: tck1.TCKBytes, 124 | J1: j1, 125 | J2: j2, 126 | Memo: memo, 127 | } 128 | 129 | return &rvk, &rak, report, nil 130 | } 131 | -------------------------------------------------------------------------------- /tcn/signedreport.go: -------------------------------------------------------------------------------- 1 | package tcn 2 | 3 | import ( 4 | "crypto" 5 | "crypto/ed25519" 6 | ) 7 | 8 | const ( 9 | // SignedReportMinLength defines a signed report's minimum length in bytes 10 | SignedReportMinLength = ReportMinLength + ed25519.SignatureSize 11 | ) 12 | 13 | // SignedReport contains a report and the corresponding signature. The client 14 | // sends this to the server. 15 | type SignedReport struct { 16 | *Report 17 | // This is an ed25519 signature in byte array form 18 | // The ed25519 package returns a byte array as the signature 19 | // here: https://golang.org/pkg/crypto/ed25519/#PrivateKey.Sign 20 | Sig []byte `db:"sig"` 21 | } 22 | 23 | // Bytes converts sr to a concatenated byte array representation. 24 | func (sr *SignedReport) Bytes() ([]byte, error) { 25 | var data []byte 26 | b, err := sr.Report.Bytes() 27 | if err != nil { 28 | return nil, err 29 | } 30 | data = append(data, b...) 31 | data = append(data, sr.Sig...) 32 | return data, nil 33 | } 34 | 35 | // GenerateSignedReport signs a report with rak and returns the signed report. 36 | func GenerateSignedReport(rak *ed25519.PrivateKey, report *Report) (*SignedReport, error) { 37 | b, err := report.Bytes() 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | sig, err := rak.Sign(nil, b, crypto.Hash(0)) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | return &SignedReport{ 48 | Report: report, 49 | Sig: sig, 50 | }, nil 51 | } 52 | 53 | // Verify uses ed25519's Verify function to verify the signature over the 54 | // report. 55 | func (sr *SignedReport) Verify() (bool, error) { 56 | reportBytes, err := sr.Report.Bytes() 57 | if err != nil { 58 | return false, err 59 | } 60 | return ed25519.Verify(sr.Report.RVK, reportBytes, sr.Sig), nil 61 | } 62 | -------------------------------------------------------------------------------- /tcn/tcn.go: -------------------------------------------------------------------------------- 1 | package tcn 2 | 3 | import ( 4 | "crypto/ed25519" 5 | "crypto/sha256" 6 | "errors" 7 | "fmt" 8 | "math" 9 | ) 10 | 11 | // HTCKDomainSep is the domain separator used for the domain-separated hash 12 | // function. 13 | const HTCKDomainSep = "H_TCK" 14 | 15 | // TemporaryContactNumber is a pseudorandom 128-bit value broadcast to nearby 16 | // devices over Bluetooth 17 | type TemporaryContactNumber [16]uint8 18 | 19 | // TemporaryContactKey is a ratcheting key used to derive temporary contact 20 | // numbers. 21 | type TemporaryContactKey struct { 22 | Index uint16 23 | RVK ed25519.PublicKey 24 | TCKBytes [32]byte 25 | } 26 | 27 | // Ratchet the key forward, producing a new key for a new temporary 28 | // contact number. 29 | func (tck *TemporaryContactKey) Ratchet() (*TemporaryContactKey, error) { 30 | nextHash := sha256.New() 31 | if _, err := nextHash.Write([]byte(HTCKDomainSep)); err != nil { 32 | fmt.Printf("Failed to write tck domain separator: %s\n", err.Error()) 33 | return nil, err 34 | } 35 | if _, err := nextHash.Write(tck.RVK); err != nil { 36 | fmt.Printf("Failed to write rvk: %s\n", err.Error()) 37 | return nil, err 38 | } 39 | if _, err := nextHash.Write(tck.TCKBytes[:]); err != nil { 40 | fmt.Printf("Failed to write tck bytes: %s\n", err.Error()) 41 | return nil, err 42 | } 43 | 44 | if tck.Index == math.MaxUint16 { 45 | return nil, errors.New("rak should be rotated") 46 | } 47 | 48 | newTCKBytes := [32]byte{} 49 | copy(newTCKBytes[:32], nextHash.Sum(nil)) 50 | 51 | return &TemporaryContactKey{ 52 | Index: tck.Index + 1, 53 | RVK: tck.RVK, 54 | TCKBytes: newTCKBytes, 55 | }, nil 56 | } 57 | --------------------------------------------------------------------------------