diff --git a/.github/.golangci.yml b/.github/.golangci.yml index 03900c6..8507b20 100644 --- a/.github/.golangci.yml +++ b/.github/.golangci.yml @@ -1,66 +1,99 @@ linters: disable-all: true enable: + - asasalint - asciicheck - bidichk - bodyclose + - canonicalheader - containedctx - contextcheck + - copyloopvar - cyclop - - deadcode + - decorder #- depguard - dogsled - dupl + - dupword - durationcheck + #- err113 - errcheck + - errchkjson - errname - errorlint + - exhaustive + - exhaustruct - exportloopref + - fatcontext - forbidigo - forcetypeassert - funlen + - gci + - ginkgolinter + - gocheckcompilerdirectives + #- gochecknoglobals + - gochecknoinits + - gochecksumtype - gocognit - goconst - gocritic - gocyclo - godot - godox - - goerr113 - gofmt - gofumpt - goheader - goimports - - gomodguard - gomoddirectives + - gomodguard - goprintffuncname - gosec - gosimple + - gosmopolitan - govet - - ifshort + - grouper - importas + - inamedparam - ineffassign + - interfacebloat + - intrange + - ireturn - lll + - loggercheck + - maintidx - makezero - #- maligned - - megacheck + - mirror - misspell + #- mnd + - musttag - nakedret - nestif - nilerr - nilnil + - nlreturn - noctx - nolintlint + #- nonamedreturns + - nosprintfhostport - paralleltest + - perfsprint - prealloc - predeclared + - promlinter + - protogetter + - reassign - revive - rowserrcheck + - sloglint + - spancheck - sqlclosecheck - staticcheck - - structcheck - stylecheck - #- tagliatelle + - tagalign + - tagliatelle - tenv + - testableexamples + - testifylint - testpackage - thelper - tparallel @@ -68,35 +101,42 @@ linters: - unconvert - unparam - unused - - varcheck + - usestdlibvars #- varnamelen - wastedassign - whitespace - #- wrapcheck + - wrapcheck - wsl - #- exhaustive - #- exhaustivestruct - #- gci - #- gochecknoglobals - #- gochecknoinits - #- gomnd - #- nlreturn - presets: - - bugs - - unused - fast: false + - zerologlint linters-settings: + cyclop: + max-complexity: 15 + skip-tests: true dupl: threshold: 100 errcheck: check-type-assertions: true check-blank: true + #exclude-functions: + # - io/ioutil.ReadFile + # - io.Copy(*bytes.Buffer) + # - io.Copy(os.Stdout) funlen: lines: 100 statements: 50 + gci: + sections: + - standard # Standard section: captures all standard packages. + - default # Default section: contains all imports that could not be matched to another section type. + - prefix(github.com/bytemare/frost) # Custom section: groups all imports with the specified Prefix. + skip-generated: true + # Enable custom order of sections. + # If `true`, make the section order the same as the order of `sections`. + # Default: false + custom-order: true gocognit: - min-complexity: 15 + min-complexity: 16 goconst: min-len: 2 min-occurrences: 2 @@ -107,6 +147,9 @@ linters-settings: - opinionated - performance - style + disabled-checks: + - unnamedResult + - sloppyReassign gocyclo: min-complexity: 15 godox: @@ -120,45 +163,80 @@ linters-settings: simplify: true goimports: local-prefixes: github.com/bytemare/frost - gomnd: - settings: - mnd: - checks: - - argument - - case - - condition - - operation - - return - - assign + gosec: + excludes: + - G115 + config: + G602: + frost: + - encodedLength + gosimple: + checks: [ "all" ] govet: - check-shadowing: true - - # settings per analyzer settings: - printf: # analyzer name, run `go tool vet help` to see all analyzers - funcs: # run `go tool vet help printf` to see available settings for `printf` analyzer - - (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof - - (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf - - (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf - - (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf - - # enable or disable analyzers by name + shadow: + strict: true + disable-all: true enable: + - asmdecl + - assign + - atomic - atomicalign - disable-all: false + - bools + - buildtag + - cgocall + - composites + - copylocks + - deepequalerrors + - errorsas + - fieldalignment + - findcall + - framepointer + - httpresponse + - ifaceassert + - loopclosure + - lostcancel + - nilfunc + - nilness + - printf + - reflectvaluecompare + - shadow + - shift + - sigchanyzer + - sortslice + - stdmethods + - stringintconv + - structtag + - testinggoroutine + - tests + - unmarshal + - unreachable + - unsafeptr + - unusedresult + - unusedwrite lll: line-length: 120 # tab width ('\t') in spaces. Default to 1. tab-width: 4 - maligned: - suggest-new: true misspell: locale: US + mnd: + checks: + - argument + - case + - condition + - operation + - return + - assign + #ignored-functions: + # - 'nist.setMapping' + # - 'big.NewInt' + # - 'hash2curve.HashToFieldXMD' + nlreturn: + block-size: 2 prealloc: simple: false for-loops: true - unused: - check-exported: false whitespace: multi-if: false multi-func: false @@ -179,18 +257,23 @@ issues: # But independently from this option we use default exclude patterns, # it can be disabled by `exclude-use-default: false`. To list all # excluded by default patterns execute `golangci-lint run --help` - exclude: - - "should have a package comment, unless it's in another file for this package" + #exclude: + #- "should have a package comment, unless it's in another file for this package" + #- "do not define dynamic errors, use wrapped static errors instead" + #- "missing cases in switch of type Group: maxID" - exclude-rules: - - path: ./* - linters: - - exhaustive + #exclude-rules: + # - path: internal/hash.go + # linters: + # - errcheck + # - path: internal/tag/strings.go + # linters: + # - gosec max-issues-per-linter: 0 max-same-issues: 0 - # Independently from option `exclude` we use default exclude patterns, + # Independently of option `exclude` we use default exclude patterns, # it can be disabled by this option. To list all # excluded by default patterns execute `golangci-lint run --help`. # Default value for this option is true. @@ -199,5 +282,7 @@ issues: run: tests: false -output: - format: github-actions \ No newline at end of file +#output: +# formats: +# - format: github-actions +# show-stats: true \ No newline at end of file diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..3b1e198 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @bytemare \ No newline at end of file diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md index 9e07407..7ddb17b 100644 --- a/.github/CODE_OF_CONDUCT.md +++ b/.github/CODE_OF_CONDUCT.md @@ -1,4 +1,5 @@ + # Contributor Covenant Code of Conduct [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.0-4baaaa.svg)](code_of_conduct.md) ## Our Pledge diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 8e40d2f..02aa318 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -25,10 +25,14 @@ Please note that changes that are purely cosmetic and do not add anything substa This project tries to be as Go idiomatic as possible. Conventions from [Effective Go](https://golang.org/doc/effective_go) apply here. Tests use a very opinionated linting configuration that you should use before committing to your changes. +## Governance Model + +This project follows the [Benevolent Dictator Governance Model](http://oss-watch.ac.uk/resources/benevolentdictatorgovernancemodel) where the project owner and lead makes all final decisions. + ### Licence By contributing to this project, you agree that your contributions will be licensed under the project's [License](https://github.com/bytemare/template/blob/main/LICENSE). -All contributions (including pull requests) must agree to the [Developer Certificate of Origin (DCO) version 1.1](http://developercertificate.org). It states that the contributor has the right to submit the patch for inclusion into the project. Simply submitting a contribution implies this agreement, however, please include the "Signed-off-by" git tag in every commit (this tag is a conventional way to confirm that you agree to the DCO). +All contributions (including pull requests) must agree to the [Developer Certificate of Origin (DCO) version 1.1](https://developercertificate.org). It states that the contributor has the right to submit the patch for inclusion into the project. Simply submitting a contribution implies this agreement, however, please include the "Signed-off-by" git tag in every commit (this tag is a conventional way to confirm that you agree to the DCO). Thanks! :heart: \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index f84c297..eaddbc7 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,5 +1,5 @@ -blank_issues_enabled: false +blank_issues_enabled: true contact_links: - name: Questions, feature requests, and more 💬 url: https://github.com/bytemare/frost/discussions - about: Do you need help? Did you make something with frost? Do you have an idea? Tell us about it! \ No newline at end of file + about: Do you need help? Did you make something with FROST? Do you have an idea? Tell us about it! \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/enhancement.md b/.github/ISSUE_TEMPLATE/enhancement.md new file mode 100644 index 0000000..8300dba --- /dev/null +++ b/.github/ISSUE_TEMPLATE/enhancement.md @@ -0,0 +1,24 @@ +--- +name: "📈 Enhancement" +about: Request or discuss improvements +title: "[Enhancement]" +labels: enhancement +assignees: bytemare + +--- + + + +### Describe the feature + +A clear and concise description of what the enhancement is and what problem it solves. + +**Expected behaviour** + +A clear and concise description of what you expected to happen. + +**Additional context** + +Add any other context about the problem here. diff --git a/.github/Makefile b/.github/Makefile index 9829570..e1a5c5b 100644 --- a/.github/Makefile +++ b/.github/Makefile @@ -1,12 +1,8 @@ -GH_ACTIONS = workflows - .PHONY: update update: - @cd ../ @echo "Updating dependencies..." + @cd ../ && go get -u ./... @go mod tidy - @echo "Updating Github Actions pins..." - @$(foreach file, $(wildcard workflows/*.yml), pin-github-action $(file);) .PHONY: update-linters update-linters: @@ -14,6 +10,8 @@ update-linters: @go install mvdan.cc/gofumpt@latest @go install github.com/daixiang0/gci@latest @go install github.com/segmentio/golines@latest + @go install github.com/google/addlicense@latest + @go install golang.org/x/tools/cmd/goimports@latest @go install golang.org/x/tools/go/analysis/passes/fieldalignment/cmd/fieldalignment@latest @curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin @@ -30,7 +28,7 @@ fmt: .PHONY: license license: @echo "Checking License headers ..." - @if addlicense -check -v -skip yml -skip yaml -f licence-header.tmpl ../*; then echo "License headers OK"; else return 1; fi; + @if addlicense -check -v -skip yaml -f licence-header.tmpl ../*; then echo "License headers OK"; else return 1; fi; .PHONY: lint lint: fmt license @@ -40,7 +38,7 @@ lint: fmt license .PHONY: test test: @echo "Running all tests ..." - @go test -v -vet=all ../tests + @go test -v ../... .PHONY: vectors vectors: @@ -50,4 +48,4 @@ vectors: .PHONY: cover cover: @echo "Testing with coverage ..." - @go test -v -race -covermode=atomic -coverpkg=../... -coverprofile=../coverage.out ../tests + @go test -v -race -covermode=atomic -coverpkg=../... -coverprofile=./coverage.out ../tests diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md index 0c4ba8a..3898f62 100644 --- a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md +++ b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md @@ -8,6 +8,8 @@ + + ### Motivation and Context @@ -26,9 +28,9 @@ ### Checklist: +- [ ] I have read the **CONTRIBUTING** document. - [ ] My code follows the code style of this project. - [ ] My change requires a change to the documentation. - [ ] I have updated the documentation accordingly. -- [ ] I have read the **CONTRIBUTING** document. - [ ] I have added tests to cover my changes. - [ ] All new and existing tests passed. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..e50a88a --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,15 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "gomod" + directory: "/" # Location of package manifests + schedule: + interval: "daily" + assignees: + - "bytemare" + reviewers: + - "bytemare" diff --git a/.github/dependency-review.yml b/.github/dependency-review.yml new file mode 100644 index 0000000..81ba9c1 --- /dev/null +++ b/.github/dependency-review.yml @@ -0,0 +1,27 @@ +# Dependency Review Action +# +# This Action will scan dependency manifest files that change as part of a Pull Request, +# surfacing known-vulnerable versions of the packages declared or updated in the PR. +# Once installed, if the workflow run is marked as required, +# PRs introducing known-vulnerable packages will be blocked from merging. +# +# Source repository: https://github.com/actions/dependency-review-action +name: 'Dependency Review' +on: [pull_request] + +permissions: + contents: read + +jobs: + dependency-review: + runs-on: ubuntu-latest + steps: + - name: Harden Runner + uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 + with: + egress-policy: block + + - name: 'Checkout Repository' + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 + - name: 'Dependency Review' + uses: actions/dependency-review-action@0efb1d1d84fc9633afcdaad14c485cbbc90ef46c diff --git a/.github/licence-header.tmpl b/.github/licence-header.tmpl index 12c483f..4d3a906 100644 --- a/.github/licence-header.tmpl +++ b/.github/licence-header.tmpl @@ -1,6 +1,6 @@ SPDX-License-Identifier: MIT -Copyright (C) 2023 Daniel Bourdrez. All Rights Reserved. +Copyright (C) 2024 Daniel Bourdrez. All Rights Reserved. This source code is licensed under the MIT license found in the LICENSE file in the root directory of this source tree or at diff --git a/.github/renovate.json b/.github/renovate.json new file mode 100644 index 0000000..d466a32 --- /dev/null +++ b/.github/renovate.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "github>bytemare/renovate-config" + ] +} diff --git a/.github/sonar-project.properties b/.github/sonar-project.properties new file mode 100644 index 0000000..1dc64cf --- /dev/null +++ b/.github/sonar-project.properties @@ -0,0 +1,8 @@ +sonar.organization=bytemare +sonar.projectKey=frost +sonar.sources=. +sonar.tests=tests/ +sonar.test.exclusions=examples_test.go,tests/** +sonar.verbose=true +sonar.coverage.exclusions=examples_test.go,tests/** +sonar.go.coverage.reportPaths=coverage.out \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 068bd26..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,84 +0,0 @@ -name: FROST -on: - pull_request: - branches: - - main - -permissions: - contents: read - -jobs: - lint: - name: Lint - runs-on: ubuntu-latest - steps: - - name: Checkout repo - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # pin@master - with: - fetch-depth: 0 - - name: Setup Go - uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 # pin@master - with: - go-version-file: ./go.mod - - # Lint - - name: Linting - uses: golangci/golangci-lint-action@5c56cd6c9dc07901af25baab6f2b0d9f3b7c3018 # pin@master - with: - version: latest - args: --config=./.github/.golangci.yml ./... - only-new-issues: true - - test: - name: Test on ${{ matrix.go }} - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - go: [ '1.21' ] - steps: - - name: Checkout repo - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # pin@master - with: - fetch-depth: 0 - - name: Setup Go - uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 # pin@master - with: - go-version: ${{ matrix.go }} - - # Test - - name: Run Tests - run: cd .github && make test - - analyze: - name: Analyze - runs-on: ubuntu-latest - steps: - - name: Checkout repo - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # pin@master - with: - fetch-depth: 0 - - name: Setup Go - uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 # pin@master - with: - go-version-file: ./go.mod - - # Coverage - - name: Run coverage - run: cd .github && make cover - - # Codecov - - name: Codecov - uses: codecov/codecov-action@29386c70ef20e286228c72b668a06fd0e8399192 # pin@master - with: - file: .github/coverage.out - - # Sonar - - name: SonarCloud Scan - uses: SonarSource/sonarcloud-github-action@5ee47de3c96f0c1c51b09d2ff1fec0cfeefcf67c # pin@master - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - with: - args: > - -Dsonar.projectKey=bytemare_frost -Dsonar.organization=bytemare-github -Dsonar.sources=. -Dsonar.go.coverage.reportPaths=.github/coverage.out -Dsonar.test.exclusions=examples_test.go,tests/** -Dsonar.tests=tests/ -Dsonar.verbose=true diff --git a/.github/workflows/code-scan.yml b/.github/workflows/code-scan.yml new file mode 100644 index 0000000..344fd9e --- /dev/null +++ b/.github/workflows/code-scan.yml @@ -0,0 +1,36 @@ +name: Code Scan + +on: + push: + branches: + - main + pull_request: + branches: + - main + schedule: + # random HH:MM to avoid a load spike on GitHub Actions at 00:00 + - cron: '4 1 * * *' + +permissions: {} + +jobs: + Lint: + permissions: + contents: read + uses: bytemare/workflows/.github/workflows/golangci-lint.yml@232148ec449718765bacb8bd4684de41f15b8258 + with: + config-path: ./.github/.golangci.yml + scope: ./... + + Analyze: + permissions: + contents: read + uses: bytemare/workflows/.github/workflows/scan-go.yml@232148ec449718765bacb8bd4684de41f15b8258 + with: + sonar-configuration: .github/sonar-project.properties + coverage-output-file: coverage.out + secrets: + github: ${{ secrets.GITHUB_TOKEN }} + sonar: ${{ secrets.SONAR_TOKEN }} + codecov: ${{ secrets.CODECOV_TOKEN }} + semgrep: ${{ secrets.SEMGREP_APP_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 1efceab..45e7530 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,39 +1,23 @@ name: "CodeQL" on: + push: + branches: + - main pull_request: branches: - main schedule: - cron: '31 10 * * 0' -permissions: - contents: read +permissions: {} jobs: - codeql: - name: CodeQL - runs-on: ubuntu-latest + CodeQL: permissions: actions: read contents: read security-events: write - - strategy: - fail-fast: false - - steps: - - name: Checkout repository - uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # pin@master - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@231aa2c8a89117b126725a0e11897209b7118144 # pin@master - with: - languages: go - - - name: Autobuild - uses: github/codeql-action/autobuild@231aa2c8a89117b126725a0e11897209b7118144 # pin@master - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@231aa2c8a89117b126725a0e11897209b7118144 # pin@master + uses: bytemare/workflows/.github/workflows/codeql.yml@232148ec449718765bacb8bd4684de41f15b8258 + with: + language: go diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 239e26e..d22d1b4 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -1,55 +1,39 @@ -name: Scorecards supply-chain security +name: Scorecard Analysis Workflow + on: - # Only the default branch is supported. - branch_protection_rule: - schedule: - - cron: '44 9 * * 0' push: - branches: [ main ] + branches: + - main + pull_request: + branches: + - main + schedule: + # Weekly on Saturdays. + - cron: '30 1 * * 6' -# Declare default permissions as read only. -permissions: read-all +permissions: {} jobs: analysis: - name: Scorecards analysis - runs-on: ubuntu-latest permissions: - # Needed to upload the results to code-scanning dashboard. + # Needed if using Code scanning alerts security-events: write + # Needed for GitHub OIDC token if publish_results is true + id-token: write + # Needed for nested workflow actions: read + attestations: read + checks: read contents: read + deployments: read + issues: read + discussions: read + packages: read + pages: read + pull-requests: read + repository-projects: read + statuses: read - steps: - - name: "Checkout code" - uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # pin@master - with: - persist-credentials: false - - - name: "Run analysis" - uses: ossf/scorecard-action@c1aec4ac820532bab364f02a81873c555a0ba3a1 # pin@master - with: - results_file: results.sarif - results_format: sarif - # Read-only PAT token. To create it, - # follow the steps in https://github.com/ossf/scorecard-action#pat-token-creation. - repo_token: ${{ secrets.SCORECARD_TOKEN }} - # Publish the results to enable scorecard badges. For more details, see - # https://github.com/ossf/scorecard-action#publishing-results. - # For private repositories, `publish_results` will automatically be set to `false`, - # regardless of the value entered here. - publish_results: true - - # Upload the results as artifacts (optional). - - name: "Upload artifact" - uses: actions/upload-artifact@82c141cc518b40d92cc801eee768e7aafc9c2fa2 # pin@master - with: - name: SARIF file - path: results.sarif - retention-days: 5 - - # Upload the results to GitHub's code scanning dashboard. - - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@5f532563584d71fdef14ee64d17bafb34f751ce5 # pin@master - with: - sarif_file: results.sarif + uses: bytemare/workflows/.github/workflows/scorecard.yml@232148ec449718765bacb8bd4684de41f15b8258 + secrets: + token: ${{ secrets.SCORECARD_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..e408132 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,22 @@ +name: Run Tests + +on: + push: + branches: + - main + pull_request: + branches: + - main + +permissions: {} + +jobs: + Test: + strategy: + fail-fast: false + matrix: + go: [ '1.22', '1.21' ] + uses: bytemare/workflows/.github/workflows/test-go.yml@232148ec449718765bacb8bd4684de41f15b8258 + with: + command: cd .github && make test + version: ${{ matrix.go }} diff --git a/.gitignore b/.gitignore index 66fd13c..bbfd2cb 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,5 @@ # Dependency directories (remove the comment below to include it) # vendor/ + +.idea \ No newline at end of file diff --git a/README.md b/README.md index ab7ffb2..5be3428 100644 --- a/README.md +++ b/README.md @@ -8,33 +8,214 @@ import "github.com/bytemare/frost" ``` -This package implements [FROST Flexible Round-Optimized Schnorr Threshold](https://datatracker.ietf.org/doc/draft-irtf-cfrg-frost) and the [FROST Distributed Key Generation](https://eprint.iacr.org/2020/852.pdf) protocols. +This package implements [RFC9591 - The FROST Flexible Round-Optimized Schnorr Threshold](https://datatracker.ietf.org/doc/rfc9591/) protocol. FROST provides Two-Round Threshold Schnorr Signatures. -#### What is frost? +The Ristretto255, Edwards25519, Secp256k1, and NIST elliptic curve groups are fully supported. -> FROST reduces network overhead during threshold signing operations while employing a novel technique to protect -> against forgery attacks applicable to prior Schnorr-based threshold signature constructions. FROST signatures can be -> issued after a threshold number of entities cooperate to compute a signature, allowing for improved distribution of -> trust and redundancy with respect to a secret key. +The [FROST Distributed Key Generation](https://github.com/bytemare/dkg) protocol produces compatible keys, as described +in the [original work](https://eprint.iacr.org/2020/852.pdf). + +### Requirements + +- When communicating at protocol execution, network channels don't need to be confidential but *MUST* be authenticated. This + package verifies a lot of things with regard to the correctness to the protocol, but it assumes that signers and coordinators + really communicate with the relevant peer. +- Long-term fixed configuration values *MUST* be known to all participant signers and coordinators (i.e. the ciphersuite, + threshold and maximum amount of signers, and the public key for signature verification) +- For every signing session, at least the public key shares of all other participants *MUST* be known to all participant + signers and coordinators (which can be a subset t-among-n of the initial key generation setup) +- Data provided to these functions (especially when received over the network) *MUST* be deserialized using the corresponding + decoding functions. If data deserialization/decoding fails for a signer, protocol execution must be aborted. +- Identifiers (for participants/signers) *MUST* be between 1 and n, which is the maximum amount of participants defined at key generation. #### Supported Ciphersuites -| ID | Name | Backend | -|-----|--------------|-------------------------------| -| 1 | Edwards25519 | filippo.io/edwards25519 | -| 2 | Ristretto255 | github.com/gtank/ristretto255 | -| 3 | Edwards448 | not yet supported | -| 4 | P-256 | filippo.io/nistec | -| 5 | Secp256k1 | github.com/bytemare/crypto | +| ID | Name | Backend | +|----|----------------------------|-------------------------------| +| 1 | Ristretto255 (recommended) | github.com/gtank/ristretto255 | +| 3 | P-256 | filippo.io/nistec | +| 4 | P-384 | filippo.io/nistec | +| 5 | P-521 | filippo.io/nistec | +| 6 | Edwards25519 | filippo.io/edwards25519 | +| 7 | Secp256k1 | github.com/bytemare/crypto | + +The groups, scalars (secret keys and nonces), and group elements (public keys and commitments) are opaque objects that +expose all necessary cryptographic and serialization functions. +If you have existing cryptographic material in their canonical encodings, they can of course be imported. + +## Usage + +Usage examples and comments can be found in [examples_test.go](https://github.com/bytemare/frost/blob/main/examples_test.go). + +### Key Generation + +The [FROST Distributed Key Generation](https://github.com/bytemare/dkg) is recommended to produce key material for all +participants in the setup. This package also puts out KeyShares and PublicKeyShares ready to use with this FROST implementation. +It also ensures correct identifier generation compatible with FROST. + +It is heavily recommended to use the same instances for distributed key generation and signing, as this will avoid that +the secret key material leaves that instance. + +For testing and debugging _only_, the [debug package](https://github.com/bytemare/frost/debug) provides a centralised +key generation with a trusted dealer. + +### Key Management + +If the [DKG](https://github.com/bytemare/dkg) package was used to generate keys, signers can use the produced KeyShare +and must communicate their PublicKeyShare to the coordinator and other signers. + +It is easy to encode and decode these key shares and public key shares for transmission and storage, +using the ```Encode()``` and ```Decode()``` methods (or in JSON marshalling). + +#### Import existing identifiers and keys + +Existing key material (e.g. identifiers, secret public, public keys) that has been generated otherwise (or transmitted or backed up) +and encoded in their canonical byte representation can be imported. + +To create a ```KeyShare``` and ```PublicKeyShare``` from individually encoded secret and public keys, use the +```keys.NewKeyShare()``` and ```NewPublicKeyShare()``` functions, respectively. +If a ```KeyShare``` or ```PublicKeyShare``` have been encoded using their respective ```Encode()``` method, they can be +easily recovered using the corresponding ```Decode()``` method. + +More generally, to decode an element (or point) in the Ristretto255 group, +```go +import ( + group "https://github.com/bytemare" +) + +bytesPublicKey := []byte{1, 2, 3, ...} + +g := group.Ristretto255Sha512 + +publicKey := g.NewElement() +if err := publicKey.Decode(bytesPublicKey); err != nil { + return fmt.Errorf("can't decode public key: %w", err) +} +``` + +The same goes for secret keys (or scalars), +```go +import ( + group "https://github.com/bytemare" +) + +bytesSecretKey := []byte{1, 2, 3, ...} + +g := group.Ristretto255Sha512 + +secretKey := g.NewScalar() +if err := secretKey.Decode(bytesSecretKey); err != nil { + return fmt.Errorf("can't decode secret key: %w", err) +} +``` + +and any other byte or json encoded structure. + +### Setup + +Both signers and coordinators must first instantiate a ```Configuration``` with the long-term fixed values as used at +key generation: +- the ciphersuite (see the frost.Ciphersuite values for available ciphersuites) +- threshold (t) and maximum amount of signers (n) +- the global public key for signature verification (as put out at key generation) + +Then add the PublicKeyShares of the participants (or signers). For simplicity, it is recommended to add all PublicKeyShares +of the all participants from the key generation step. It is sufficient, though, to only use the shares for the signers that +will participate in a signing session (which can be a subset _t among n_). + +```go +configuration := &frost.Configuration{ + Ciphersuite: ciphersuite, + Threshold: threshold, + MaxSigners: maxSigners, + GroupPublicKey: groupPublicKey, + SignerPublicKeyShares: publicKeyShares, + } + +if err := configuration.Init(); err != nil { + return err +} +``` + +This configuration can be encoded for transmission and offline storage, and re-instantiated using its +```Encode()``` and ```Decode()``` methods. This avoids having to store the parameters separately. + +#### Signers + +Once the configuration is initialised, setting up a signer is straightforward, using the ```Signer()``` method +and providing the signer's ```KeyShare```. + +### Protocol execution + +FROST is a two round signing protocol, in which the first round can be asynchronously pre-computed, so that signing can +actually be done in one round when necessary. + +First Round: Signer commitment +- Signers commit to internal nonces, by calling the ```commitment := signer.Commit()``` method, which returns one commitment +stores corresponding nonces internally. In this manner, signers can produce many commitments before signing sessions start. +Signers send these commitments to either a coordinator or all other signers. Note that a commitment is not function of the +future message to sign, so a signer can produce them without knowing the message in advance. +- The coordinator (or all other signers) collect these commitments, into a list. + +Second Round: Signing +- The coordinator broadcasts the message to be signed and a list of commitments, one from each signer, to each signer. +- The signers sign the message ```sigShare, err := signer.Sign(message, commitmentList)```, each producing their signature share. +- These signature shares must then be shared and aggregated to produce the final signature, ```signature, err := configuration.AggregateSignatures(message, sigShares, commitmentList, true)```. + +#### Coordinator + +The coordinator does not have any secret or private information, and must never have. It is also assumed to behave honestly. + +Commitments received by signers have an identifier, which allows for triage and registration. Commitments must only be +used once. The coordinator may further hedge against nonce-reuse by tracking the nonce commitments used for a given group key. + +If the ```verify``` argument in the ```AggregateSignatures()``` is set to ```true``` (recommended), signature shares and output signature are thoroughly verified. +Upon error or invalid share, the error message indicates the first invalid share it encountered. +A coordinator should always verify the signature after ```AggregateSignatures()``` if the ```verify``` argument has been set to ```false```. + +If verification fails, the coordinator can then check signature shares individually to deter the misbehaving signer, leveraging the authenticated channel associated to them. +That signer can then be denied of further contributions. + +### Resumption and storage + +Configurations, keys, commitments, commitment lists, and even signers can be backed up to byte strings for transmission and storage, +and re-instantiated from them. To decode, just create that object and use its ```Decode()``` method. + +For example, to back up a signer with its private keys and commitments, use: +```go +bytes := signer.Encode() +``` + +To re-instantiate that same signer from the byte string, do: +```go +// bytes := signer.Encode() + +signer := new(frost.Signer) +if err := signer.Decode(bytes); err != nil { + return err +} +``` + +Keep in mind that signer encoding embeds the private key and secret nonces, and that they must be secured accordingly. + +## Notes + +Signers have local secret data and state, offline and during protocol execution: +- the long term secret key +- the internally stored commitment nonces, maintained between commitment and signature -#### References -- [The original paper](https://eprint.iacr.org/2020/852.pdf) from Chelsea Komlo and Ian Goldberg. -- [The Github repo](https://github.com/cfrg/draft-irtf-cfrg-frost) where the draft is being specified. +- FROST is _not robust_ by design. + - This means that there is a misbehaving participant if signature aggregation fails + (or if the output signature is not valid), in which case the protocol should be aborted and the problem investigated + (you shouldn't have a compromised or misbehaving participant in a sane infrastructure). + - Misbehaving signers can DOS the protocol by providing wrong sig shares or not contributing. +- The coordinator may further hedge against nonce-reuse by tracking the nonce commitments used for a given group key +- For message pre-hashing, see [RFC](https://datatracker.ietf.org/doc/rfc9591) ## Documentation [![Go Reference](https://pkg.go.dev/badge/github.com/bytemare/frost.svg)](https://pkg.go.dev/github.com/bytemare/frost) -You can find the documentation and usage examples in [the package doc](https://pkg.go.dev/github.com/bytemare/frost). +You can find the godoc documentation and usage examples in [the package doc](https://pkg.go.dev/github.com/bytemare/frost). ## Versioning diff --git a/commitment.go b/commitment.go index 0d16bb8..cabd930 100644 --- a/commitment.go +++ b/commitment.go @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT // -// Copyright (C) 2023 Daniel Bourdrez. All Rights Reserved. +// Copyright (C) 2024 Daniel Bourdrez. All Rights Reserved. // // This source code is licensed under the MIT license found in the // LICENSE file in the root directory of this source tree or at @@ -9,122 +9,376 @@ package frost import ( + "encoding/binary" "errors" "fmt" "slices" group "github.com/bytemare/crypto" + secretsharing "github.com/bytemare/secret-sharing" "github.com/bytemare/frost/internal" ) -var errDecodeCommitmentLength = errors.New("failed to decode commitment: invalid length") +var ( + errDecodeCommitmentLength = errors.New("failed to decode commitment: invalid length") + errInvalidCiphersuite = errors.New("ciphersuite not available") + errInvalidLength = errors.New("invalid encoding length") + errCommitmentNil = errors.New("the commitment is nil") + errCommitmentListEmpty = errors.New("commitment list is empty") + errCommitmentListNotSorted = errors.New("commitment list is not sorted by signer identifiers") + errCommitmentListHasNil = errors.New("the commitment list has a nil commitment") +) // Commitment is a participant's one-time commitment holding its identifier, and hiding and binding nonces. type Commitment struct { - Identifier *group.Scalar - HidingNonce *group.Element - BindingNonce *group.Element + HidingNonceCommitment *group.Element + BindingNonceCommitment *group.Element + CommitmentID uint64 + SignerID uint64 + Group group.Group } -// Encode returns the serialized byte encoding of a participant's commitment. -func (c Commitment) Encode() []byte { - id := c.Identifier.Encode() - hNonce := c.HidingNonce.Encode() - bNonce := c.BindingNonce.Encode() +// Copy returns a new Commitment struct populated with the same values as the receiver. +func (c *Commitment) Copy() *Commitment { + return &Commitment{ + HidingNonceCommitment: c.HidingNonceCommitment.Copy(), + BindingNonceCommitment: c.BindingNonceCommitment.Copy(), + CommitmentID: c.CommitmentID, + SignerID: c.SignerID, + Group: c.Group, + } +} + +// CommitmentList is a sortable list of commitments with search functions. +type CommitmentList []*Commitment + +// cmpID returns a negative number when the signer identity of a < b, a positive number when +// a > b and zero when a == b. +func cmpID(a, b *Commitment) int { + switch { + case a.SignerID < b.SignerID: // a < b + return -1 + case a.SignerID > b.SignerID: + return 1 + default: + return 0 + } +} + +// Sort sorts the list the ascending order of identifiers. +func (c CommitmentList) Sort() { + if !c.IsSorted() { + slices.SortFunc(c, cmpID) + } +} + +// IsSorted returns whether the list is sorted in ascending order by identifier. +func (c CommitmentList) IsSorted() bool { + return slices.IsSortedFunc(c, cmpID) +} + +// Get returns the commitment of the participant with the corresponding identifier, or nil if it was not found. +func (c CommitmentList) Get(identifier uint64) *Commitment { + for _, com := range c { + if com.SignerID == identifier { + return com + } + } + + return nil +} - out := make([]byte, len(id)+len(hNonce)+len(bNonce)) - copy(out, id) - copy(out[len(id):], hNonce) - copy(out[len(id)+len(hNonce):], bNonce) +// Participants returns the uint64 list of participant identifiers in the list. +func (c CommitmentList) Participants() []uint64 { + out := make([]uint64, len(c)) + + for i, com := range c { + out[i] = com.SignerID + } return out } -// DecodeCommitment attempts to deserialize the encoded commitment given as input, and to return it. -func DecodeCommitment(cs Ciphersuite, data []byte) (*Commitment, error) { - g := cs.Configuration().Ciphersuite.Group - scalarLength := g.ScalarLength() - elementLength := g.ElementLength() +// ParticipantsScalar returns the group.Scalar list of participant identifier in the list. +func (c CommitmentList) ParticipantsScalar() []*group.Scalar { + if len(c) == 0 { + return nil + } + + if c[0] == nil { + return nil + } + + g := c[0].Group + + return secretsharing.NewPolynomialFromListFunc(g, c, func(c *Commitment) *group.Scalar { + return g.NewScalar().SetUInt64(c.SignerID) + }) +} + +// Encode serializes the CommitmentList into a compact byte encoding. +func (c CommitmentList) Encode() []byte { + n := len(c) + if n == 0 { + return nil + } + + g := c[0].Group + size := 1 + 8 + uint64(n)*encodedLength(encCommitment, g) + out := make([]byte, 9, size) + out[0] = byte(g) + binary.LittleEndian.PutUint64(out[1:9], uint64(n)) - if len(data) != scalarLength+2*elementLength { - return nil, errDecodeCommitmentLength + for _, com := range c { + out = append(out, com.Encode()...) } - c := &Commitment{ - Identifier: g.NewScalar(), - HidingNonce: g.NewElement(), - BindingNonce: g.NewElement(), + return out +} + +// DecodeList decodes a byte string produced by the CommitmentList.Encode() method. +func DecodeList(data []byte) (CommitmentList, error) { + if len(data) < 9 { + return nil, errInvalidLength } - if err := c.Identifier.Decode(data[:scalarLength]); err != nil { - return nil, fmt.Errorf("failed to decode commitment identifier: %w", err) + g := group.Group(data[0]) + if !g.Available() { + return nil, errInvalidCiphersuite } - if err := c.HidingNonce.Decode(data[:scalarLength]); err != nil { - return nil, fmt.Errorf("failed to decode commitment hiding nonce: %w", err) + n := binary.LittleEndian.Uint64(data[1:9]) + es := encodedLength(encCommitment, g) + size := 1 + 8 + n*es + + if uint64(len(data)) != size { + return nil, errInvalidLength } - if err := c.BindingNonce.Decode(data[:scalarLength]); err != nil { - return nil, fmt.Errorf("failed to decode commitment binding nonce: %w", err) + c := make(CommitmentList, 0, n) + + for offset := uint64(9); offset < uint64(len(data)); offset += es { + com := new(Commitment) + if err := com.Decode(data[offset : offset+es]); err != nil { + return nil, fmt.Errorf("invalid encoding of commitment: %w", err) + } + + c = append(c, com) } return c, nil } -// CommitmentList is a sortable list of commitments. -type CommitmentList []*Commitment +func (c CommitmentList) groupCommitmentAndBindingFactors( + publicKey *group.Element, + message []byte, +) (*group.Element, BindingFactors) { + bindingFactors := c.bindingFactors(publicKey, message) + groupCommitment := c.groupCommitment(bindingFactors) -func cmpID(a, b *Commitment) int { - switch { - case a.Identifier.Equal(b.Identifier) == 1: // a == b - return 0 - case a.Identifier.LessOrEqual(b.Identifier) == 1: // a < b - return -1 - default: - return 1 - } + return groupCommitment, bindingFactors } -// Sort sorts the list the ascending order of identifiers. -func (c CommitmentList) Sort() { - slices.SortFunc(c, cmpID) +type commitmentWithEncodedID struct { + *Commitment + ParticipantID []byte } -// IsSorted returns whether the list is sorted in ascending order by identifier. -func (c CommitmentList) IsSorted() bool { - return slices.IsSortedFunc(c, cmpID) +func commitmentsWithEncodedID(g group.Group, commitments CommitmentList) []*commitmentWithEncodedID { + r := make([]*commitmentWithEncodedID, len(commitments)) + for i, com := range commitments { + r[i] = &commitmentWithEncodedID{ + ParticipantID: g.NewScalar().SetUInt64(com.SignerID).Encode(), + Commitment: com, + } + } + + return r } -// Encode serializes a whole commitment list. -func (c CommitmentList) Encode() []byte { - var encoded []byte +func encodeCommitmentList(g group.Group, commitments []*commitmentWithEncodedID) []byte { + size := len(commitments) * (g.ScalarLength() + 2*g.ElementLength()) + encoded := make([]byte, 0, size) - for _, l := range c { - e := internal.Concatenate(l.Identifier.Encode(), l.HidingNonce.Encode(), l.BindingNonce.Encode()) - encoded = append(encoded, e...) + for _, com := range commitments { + encoded = append(encoded, com.ParticipantID...) + encoded = append(encoded, com.HidingNonceCommitment.Encode()...) + encoded = append(encoded, com.BindingNonceCommitment.Encode()...) } return encoded } -// Participants returns the list of participants in the commitment list. -func (c CommitmentList) Participants() []*group.Scalar { - identifiers := make([]*group.Scalar, len(c)) - for i, l := range c { - identifiers[i] = l.Identifier +// BindingFactors is a map of participant identifiers to BindingFactors. +type BindingFactors map[uint64]*group.Scalar + +func (c CommitmentList) bindingFactors(publicKey *group.Element, message []byte) BindingFactors { + g := c[0].Group + coms := commitmentsWithEncodedID(g, c) + encodedCommitHash := internal.H5(g, encodeCommitmentList(g, coms)) + h := internal.H4(g, message) + rhoInputPrefix := internal.Concatenate(publicKey.Encode(), h, encodedCommitHash) + bindingFactors := make(BindingFactors, len(c)) + + for _, com := range coms { + rhoInput := internal.Concatenate(rhoInputPrefix, com.ParticipantID) + bindingFactors[com.Commitment.SignerID] = internal.H1(g, rhoInput) } - return identifiers + return bindingFactors } -// Get returns the commitment of the participant with the corresponding identifier, or nil if it was not found. -func (c CommitmentList) Get(identifier *group.Scalar) *Commitment { +func (c CommitmentList) groupCommitment(bf BindingFactors) *group.Element { + g := c[0].Group + gc := g.NewElement() + for _, com := range c { - if com.Identifier.Equal(identifier) == 1 { - return com + factor := bf[com.SignerID] + bindingNonce := com.BindingNonceCommitment.Copy().Multiply(factor) + gc.Add(com.HidingNonceCommitment).Add(bindingNonce) + } + + return gc +} + +func (c *Configuration) isSignerRegistered(sid uint64) bool { + for _, peer := range c.SignerPublicKeyShares { + if peer.ID == sid { + return true } } + return false +} + +// ValidateCommitment returns an error if the commitment is not valid. +func (c *Configuration) ValidateCommitment(commitment *Commitment) error { + if !c.verified || !c.keysVerified { + if err := c.Init(); err != nil { + return err + } + } + + if commitment == nil { + return errCommitmentNil + } + + if err := c.validateIdentifier(commitment.SignerID); err != nil { + return fmt.Errorf("invalid identifier for signer in commitment %d, the %w", commitment.CommitmentID, err) + } + + if commitment.Group != c.group { + return fmt.Errorf( + "commitment %d for participant %d has an unexpected ciphersuite: expected %s, got %d", + commitment.CommitmentID, + commitment.SignerID, + c.group, + commitment.Group, + ) + } + + if err := c.validateGroupElement(commitment.HidingNonceCommitment); err != nil { + return fmt.Errorf( + "invalid commitment %d for signer %d, the hiding nonce commitment %w", + commitment.CommitmentID, + commitment.SignerID, + err, + ) + } + + if err := c.validateGroupElement(commitment.BindingNonceCommitment); err != nil { + return fmt.Errorf( + "invalid commitment %d for signer %d, the binding nonce commitment %w", + commitment.CommitmentID, + commitment.SignerID, + err, + ) + } + + // Validate that the commitment comes from a registered signer. + if !c.isSignerRegistered(commitment.SignerID) { + return fmt.Errorf( + "signer identifier %d for commitment %d is not registered in the configuration", + commitment.SignerID, + commitment.CommitmentID, + ) + } + + return nil +} + +func (c *Configuration) validateCommitmentListLength(commitments CommitmentList) error { + length := uint64(len(commitments)) + + if length == 0 { + return errCommitmentListEmpty + } + + if length < c.Threshold { + return fmt.Errorf("too few commitments: expected at least %d but got %d", c.Threshold, length) + } + + if length > c.MaxSigners { + return fmt.Errorf("too many commitments: expected %d or less but got %d", c.MaxSigners, length) + } + + return nil +} + +// ValidateCommitmentList returns an error if at least one of the following conditions is not met: +// - list length is within [threshold;max]. +// - no signer identifier in commitments is 0. +// - no singer identifier in commitments is > max signers. +// - no duplicated in signer identifiers. +// - all commitment signer identifiers are registered in the configuration. +func (c *Configuration) ValidateCommitmentList(commitments CommitmentList) error { + if !c.verified || !c.keysVerified { + if err := c.Init(); err != nil { + return err + } + } + + if err := c.validateCommitmentListLength(commitments); err != nil { + return err + } + + // set to detect duplication + set := make(map[uint64]struct{}, len(commitments)) + + for i, commitment := range commitments { + // Check general validity of the commitment. + if err := c.ValidateCommitment(commitment); err != nil { + return err + } + + // Check for duplicate participant entries. + if _, exists := set[commitment.SignerID]; exists { + return fmt.Errorf("commitment list contains multiple commitments of participant %d", commitment.SignerID) + } + + set[commitment.SignerID] = struct{}{} + + // List must be sorted, compare with the next commitment. + if i <= len(commitments)-2 { + if err := compareCommitments(commitment, commitments[i+1]); err != nil { + return err + } + } + } + + return nil +} + +func compareCommitments(c1, c2 *Commitment) error { + if c2 == nil { + return errCommitmentListHasNil + } + + // if the current commitment has an id higher than the next one, return error. + if cmpID(c1, c2) > 0 { + return errCommitmentListNotSorted + } + return nil } diff --git a/coordinator.go b/coordinator.go index 06f4260..157c050 100644 --- a/coordinator.go +++ b/coordinator.go @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT // -// Copyright (C) 2023 Daniel Bourdrez. All Rights Reserved. +// Copyright (C) 2024 Daniel Bourdrez. All Rights Reserved. // // This source code is licensed under the MIT license found in the // LICENSE file in the root directory of this source tree or at @@ -9,11 +9,23 @@ package frost import ( + "errors" + "fmt" + group "github.com/bytemare/crypto" - secretsharing "github.com/bytemare/secret-sharing" + + "github.com/bytemare/frost/internal" ) -// Aggregate allows the coordinator to produce the final signature given all signature shares. +var errInvalidSignature = errors.New("invalid Signature") + +// Signature represent a Schnorr signature. +type Signature struct { + R *group.Element + Z *group.Scalar +} + +// AggregateSignatures enables a coordinator to produce the final signature given all signature shares. // // Before aggregation, each signature share must be a valid, deserialized element. If that validation fails the // coordinator must abort the protocol, as the resulting signature will be invalid. @@ -21,74 +33,169 @@ import ( // // The coordinator should verify this signature using the group public key before publishing or releasing the signature. // This aggregate signature will verify if and only if all signature shares are valid. If an invalid share is identified -// a reasonable approach is to remove the participant from the set of allowed participants in future runs of FROST. -func (p *Participant) Aggregate( - list CommitmentList, - msg []byte, +// a reasonable approach is to remove the signer from the set of allowed participants in future runs of FROST. If verify +// is set to true, AggregateSignatures will automatically verify the signature shares and the output signature, and will +// return an error with the first encountered invalid signature share. +func (c *Configuration) AggregateSignatures( + message []byte, sigShares []*SignatureShare, -) *Signature { - if !list.IsSorted() { - panic("list not sorted") + commitments CommitmentList, + verify bool, +) (*Signature, error) { + if !c.verified || !c.keysVerified { + if err := c.Init(); err != nil { + return nil, err + } + } + + groupCommitment, bindingFactors, participants, err := c.prepareSignatureShareVerification(message, commitments) + if err != nil { + return nil, err } - // Compute binding factors - bindingFactorList := p.computeBindingFactors(list, msg) + if verify { + for _, share := range sigShares { + if err = c.verifySignatureShare(share, message, commitments, participants, + groupCommitment, bindingFactors); err != nil { + return nil, err + } + } + } - // Compute group commitment - groupCommitment := p.computeGroupCommitment(list, bindingFactorList) + // Aggregate signatures. + signature, err := c.sumShares(sigShares, groupCommitment) + if err != nil { + return nil, err + } - // Compute aggregate signature - z := p.Ciphersuite.Group.NewScalar() - for _, share := range sigShares { - z.Add(share.SignatureShare) + // Verify the final signature. + if verify { + if err = VerifySignature(c.Ciphersuite, message, signature, c.GroupPublicKey); err != nil { + // difficult to reach, because if all shares are valid, the final signature is valid. + return nil, err + } + } + + return signature, nil +} + +func (c *Configuration) sumShares(shares []*SignatureShare, groupCommitment *group.Element) (*Signature, error) { + z := group.Group(c.Ciphersuite).NewScalar() + + for _, sigShare := range shares { + if err := c.validateSignatureShareLight(sigShare); err != nil { + return nil, err + } + + z.Add(sigShare.SignatureShare) } return &Signature{ R: groupCommitment, Z: z, - } + }, nil } -// VerifySignatureShare verifies a signature share. -// id, pki, commi, and sigShareI are, respectively, the identifier, public key, commitment, and signature share of -// the participant whose share is to be verified. +// VerifySignatureShare verifies a signature share. sigShare is the signer's signature share to be verified. // // The CommitmentList must be sorted in ascending order by identifier. -func (p *Participant) VerifySignatureShare( - commitment *Commitment, - pki *group.Element, - sigShareI *group.Scalar, - coms CommitmentList, - msg []byte, -) bool { - if !coms.IsSorted() { - panic("list not sorted") +func (c *Configuration) VerifySignatureShare( + sigShare *SignatureShare, + message []byte, + commitments CommitmentList, +) error { + if !c.verified || !c.keysVerified { + if err := c.Init(); err != nil { + return err + } } - // Compute Binding Factor(s) - bindingFactorList := p.computeBindingFactors(coms, msg) - bindingFactor := bindingFactorList.BindingFactorForParticipant(commitment.Identifier) + groupCommitment, bindingFactors, participants, err := c.prepareSignatureShareVerification(message, commitments) + if err != nil { + return err + } - // Compute Group Commitment - groupCommitment := p.computeGroupCommitment(coms, bindingFactorList) + return c.verifySignatureShare(sigShare, message, commitments, participants, groupCommitment, bindingFactors) +} - // Commitment KeyShare - commShare := commitment.HidingNonce.Copy().Add(commitment.BindingNonce.Copy().Multiply(bindingFactor)) +func (c *Configuration) prepareSignatureShareVerification(message []byte, + commitments CommitmentList, +) (*group.Element, BindingFactors, []*group.Scalar, error) { + commitments.Sort() - // Compute the challenge - challenge := challenge(p.Ciphersuite, groupCommitment, p.Configuration.GroupPublicKey, msg) + // Validate general consistency of the commitment list. + if err := c.ValidateCommitmentList(commitments); err != nil { + return nil, nil, nil, fmt.Errorf("invalid list of commitments: %w", err) + } - // Compute the interpolating value - participantList := secretsharing.Polynomial(coms.Participants()) + groupCommitment, bindingFactors := commitments.groupCommitmentAndBindingFactors(c.GroupPublicKey, message) + participants := commitments.ParticipantsScalar() - lambdaI, err := participantList.DeriveInterpolatingValue(p.Ciphersuite.Group, commitment.Identifier) - if err != nil { - panic(err) + return groupCommitment, bindingFactors, participants, nil +} + +func (c *Configuration) validateSignatureShareLight(sigShare *SignatureShare) error { + if sigShare == nil { + return errors.New("nil signature share") + } + + if sigShare.SignatureShare == nil || sigShare.SignatureShare.IsZero() { + return errors.New("invalid signature share (nil or zero scalar)") + } + + return nil +} + +func (c *Configuration) validateSignatureShareExtensive(sigShare *SignatureShare) error { + if err := c.validateSignatureShareLight(sigShare); err != nil { + return err + } + + if err := c.validateIdentifier(sigShare.SignerIdentifier); err != nil { + return fmt.Errorf("invalid identifier for signer in signature share, the %w", err) + } + + if sigShare.Group != c.group { + return fmt.Errorf("signature share has invalid group parameter, want %s got %d", c.group, sigShare.Group) } - // Compute relation values - l := p.Ciphersuite.Group.Base().Multiply(sigShareI) - r := commShare.Add(pki.Multiply(challenge.Multiply(lambdaI))) + if c.getSignerPubKey(sigShare.SignerIdentifier) == nil { + return fmt.Errorf("no public key registered for signer %d", sigShare.SignerIdentifier) + } + + return nil +} + +func (c *Configuration) verifySignatureShare( + sigShare *SignatureShare, + message []byte, + commitments CommitmentList, + participants []*group.Scalar, + groupCommitment *group.Element, + bindingFactors BindingFactors, +) error { + if err := c.validateSignatureShareExtensive(sigShare); err != nil { + return err + } + + com := commitments.Get(sigShare.SignerIdentifier) + if com == nil { + return fmt.Errorf("commitment for signer %d is missing", sigShare.SignerIdentifier) + } + + pk := c.getSignerPubKey(sigShare.SignerIdentifier) + lambda := internal.Lambda(c.group, sigShare.SignerIdentifier, participants) + lambdaChall := c.challenge(lambda, message, groupCommitment) + + // Commitment KeyShare: r = g(h + b*f + l*s) + bindingFactor := bindingFactors[sigShare.SignerIdentifier] + commShare := com.HidingNonceCommitment.Copy().Add(com.BindingNonceCommitment.Copy().Multiply(bindingFactor)) + r := commShare.Add(pk.Copy().Multiply(lambdaChall)) + l := c.group.Base().Multiply(sigShare.SignatureShare) + + if l.Equal(r) != 1 { + return fmt.Errorf("invalid signature share for signer %d", sigShare.SignerIdentifier) + } - return l.Equal(r) == 1 + return nil } diff --git a/debug/debug.go b/debug/debug.go new file mode 100644 index 0000000..1892641 --- /dev/null +++ b/debug/debug.go @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: MIT +// +// Copyright (C) 2024 Daniel Bourdrez. All Rights Reserved. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree or at +// https://spdx.org/licenses/MIT.html + +// Package debug provides tools for key generation and verification for debugging purposes. They might be helpful for +// setups and investigations, but are not recommended to be used with production data (e.g. centralized key generation +// or recovery reveals the group's secret key in one spot, which goes against the principle in a decentralized setup). +package debug + +import ( + "fmt" + + group "github.com/bytemare/crypto" + secretsharing "github.com/bytemare/secret-sharing" + + "github.com/bytemare/frost" + "github.com/bytemare/frost/internal" + "github.com/bytemare/frost/keys" +) + +// TrustedDealerKeygen uses Shamir and Verifiable Secret Sharing to create secret shares of an input group secret. If +// secret is not set, a new random secret will be generated. +// These shares should be distributed securely to relevant participants. Note that this is centralized and combines +// the shared secret at some point. To use a decentralized dealer-less key generation, use the github.com/bytemare/dkg +// package. +func TrustedDealerKeygen( + c frost.Ciphersuite, + secret *group.Scalar, + threshold, maxSigners uint64, + coeffs ...*group.Scalar, +) ([]*keys.KeyShare, *group.Element, []*group.Element) { + g := group.Group(c) + + if secret == nil { + // If no secret provided, generated a new random secret. + g.NewScalar().Random() + } + + privateKeyShares, poly, err := secretsharing.ShardReturnPolynomial( + g, + secret, + uint(threshold), + uint(maxSigners), + coeffs...) + if err != nil { + panic(err) + } + + coms := secretsharing.Commit(g, poly) + + shares := make([]*keys.KeyShare, maxSigners) + for i, k := range privateKeyShares { + shares[i] = &keys.KeyShare{ + Secret: k.Secret, + GroupPublicKey: coms[0], + PublicKeyShare: secretsharing.PublicKeyShare{ + PublicKey: g.Base().Multiply(k.Secret), + Commitment: coms, + ID: k.ID, + Group: g, + }, + } + } + + return shares, coms[0], coms +} + +// RecoverGroupSecret returns the groups secret from at least t-among-n (t = threshold) participant key shares. This is +// not recommended, as combining all distributed secret shares can put the group secret at risk. +func RecoverGroupSecret(c frost.Ciphersuite, keyShares []*keys.KeyShare) (*group.Scalar, error) { + if !c.Available() { + return nil, internal.ErrInvalidCiphersuite + } + + g := group.Group(c) + + publicKeys := make([]secretsharing.Share, len(keyShares)) + for i, v := range keyShares { + publicKeys[i] = v + } + + secret, err := secretsharing.CombineShares(g, publicKeys) + if err != nil { + return nil, fmt.Errorf("failed to reconstruct group secret: %w", err) + } + + return secret, nil +} + +// Sign returns a Schnorr signature over the message msg with the full secret signing key (as opposed to a key share). +// The optional random argument is the random k in Schnorr signatures. Setting it allows for reproducible signatures. +func Sign(c frost.Ciphersuite, msg []byte, key *group.Scalar, random ...*group.Scalar) (*frost.Signature, error) { + g := c.ECGroup() + if g == 0 { + return nil, internal.ErrInvalidCiphersuite + } + + var k *group.Scalar + + if len(random) != 0 && random[0] != nil { + k = random[0].Copy() + } else { + k = g.NewScalar().Random() + } + + R := g.Base().Multiply(k) + pk := g.Base().Multiply(key) + challenge := frost.SchnorrChallenge(g, msg, R, pk) + z := k.Add(challenge.Multiply(key)) + + return &frost.Signature{ + R: R, + Z: z, + }, nil +} + +// RecoverPublicKeys returns the group public key as well those from all participants, +// if the identifiers are 1, 2, ..., maxSigners, given the VSS commitment vector. +func RecoverPublicKeys( + c frost.Ciphersuite, + maxSigners uint64, + commitment []*group.Element, +) (*group.Element, []*group.Element, error) { + if !c.Available() { + return nil, nil, internal.ErrInvalidCiphersuite + } + + g := group.Group(c) + publicKeys := make([]*group.Element, maxSigners) + + for i := uint64(1); i <= maxSigners; i++ { + pki, err := secretsharing.PubKeyForCommitment(g, i, commitment) + if err != nil { + return nil, nil, fmt.Errorf("can't recover public keys: %w", err) + } + + publicKeys[i-1] = pki + } + + return commitment[0], publicKeys, nil +} + +// VerifyVSS allows verification of a participant's secret share given a VSS commitment to the secret polynomial. +func VerifyVSS(g group.Group, share *keys.KeyShare, commitment []*group.Element) bool { + pk := g.Base().Multiply(share.SecretKey()) + return secretsharing.Verify(g, share.Identifier(), pk, commitment) +} diff --git a/dkg/README.md b/dkg/README.md deleted file mode 100644 index 116a709..0000000 --- a/dkg/README.md +++ /dev/null @@ -1,55 +0,0 @@ -# Distributed Key Generation - -This package implements a Distributed Key Generation. It builds on the 2-round Pederson DGK and extends it with -zero-knowledge proofs to protect against rogue-key attacks, as defined in [FROST](https://eprint.iacr.org/2020/852.pdf). - -This effectively generates keys among participants without the need of a trusted dealer or third-party. These keys are -compatible for use in FROST. - -### References - -- Pederson introduced the [first DKG protocol](https://link.springer.com/chapter/10.1007/3-540-46416-6_47), based on Feldman's Verifiable Secret Sharing. -- Komlo & Goldberg [add zero-knowledge proofs](https://eprint.iacr.org/2020/852.pdf) to the Ped-DKG. - -## Usage - -### Assumptions - -- All parties are identified with unique IDs. -- Communicate over confidential, authenticated, and secure channels. -- All participants honestly follow the protocol (they can, nevertheless, identify the misbehaving participant). - -### Setup - -Use the same ciphersuite for the DKG and FROST. - -### Error handling - -In case of an identified misbehaving participant, abort the protocol immediately. If this happens there might be a serious -problem that must be investigated. One may re-run the protocol after excluding that participant and solving the problem. - -### Protocol - -The following steps describe how to run the DKG among participants. For each participant: -1. Run Init() - - this returns a round 1 package - - send/broadcast this package to every participant - (this might include the very same participant, in which case it should discard it) -2. Collect all the r1 packages from other participants -3. Run Continue() with the collection of r1 packages - - this returns round 2 packages, one destined to each other participant - - send these packages to their destined participant -4. Collect all round 2 packages destined to the participant -5. Run Finalize() with the collected round 1 and round 2 packages - - returns the participant's own secret signing share, - the corresponding verification share, and the group's public key -6. Erase all intermediary values received and computed by the participants (including in their states) -7. Optionally, compute the verification keys for each other participant and store them - -## Possible extensions - -- Laing and Stinson [refine Repairable Threshold Schemes](https://eprint.iacr.org/2017/1155.pdf) to enable a participant to securely reconstruct a lost share with help from their peers. -- Herzberg et al. propose [Proactive Secret Sharing](https://www.researchgate.net/profile/Amir-Herzberg/publication/221355399_Proactive_Secret_Sharing_Or_How_to_Cope_With_Perpetual_Leakage/links/02e7e52e0ecf4dbae1000000/Proactive-Secret-Sharing-Or-How-to-Cope-With-Perpetual-Leakage.pdf), allowing for shares to be rotated without impact on the secret key. -- Gennaro et al. improve on the Ped-DKG and propose a [more robust version called New-DKG](https://link.springer.com/article/10.1007/s00145-006-0347-3). -- Canetti et al. extend New-DKG to make it [secure against adaptive adversaries](https://link.springer.com/content/pdf/10.1007/3-540-48405-1_7.pdf). -- Jarecki and Lysyanskaya present the [erasure-free model](https://www.iacr.org/archive/eurocrypt2000/1807/18070223-new.pdf) for threshold schemes secure against adaptive adversaries. diff --git a/dkg/dkg.go b/dkg/dkg.go deleted file mode 100644 index a9e64cb..0000000 --- a/dkg/dkg.go +++ /dev/null @@ -1,254 +0,0 @@ -// SPDX-License-Identifier: MIT -// -// Copyright (C) 2023 Daniel Bourdrez. All Rights Reserved. -// -// This source code is licensed under the MIT license found in the -// LICENSE file in the root directory of this source tree or at -// https://spdx.org/licenses/MIT.html - -// Package dkg implements the Distributed Key Generation described in FROST, -// using zero-knowledge proofs in Schnorr signatures. -package dkg - -import ( - "encoding/hex" - "errors" - "fmt" - - group "github.com/bytemare/crypto" - secretsharing "github.com/bytemare/secret-sharing" - - "github.com/bytemare/frost" - "github.com/bytemare/frost/internal" -) - -var ( - errRound1DataElements = errors.New("invalid number of expected round 1 data packets") - errRound2DataElements = errors.New("invalid number of expected round 2 data packets") - errRound2InvalidReceiver = errors.New("invalid receiver in round 2 package") - errInvalidSignature = errors.New("invalid signature") - - errCommitmentNotFound = errors.New("commitment not found for participant") - errInvalidSecretShare = errors.New("invalid secret share received from peer") - errVerificationShareFailed = errors.New("failed to compute correct verification share") -) - -// Round1Data is the output data of the Init() function, to be broadcast to all participants. -type Round1Data struct { - ProofOfKnowledge frost.Signature - SenderIdentifier *group.Scalar - Commitment []*group.Element -} - -// Round2Data is an output of the Continue() function, to be sent to the Receiver. -type Round2Data struct { - SenderIdentifier *group.Scalar - - ReceiverIdentifier *group.Scalar - SecretShare *group.Scalar -} - -// Participant represent a party in the Distributed Key Generation. Once the DKG completed, all values must be erased. -type Participant struct { - Identifier *group.Scalar - publicShare *group.Element - secretShare *group.Scalar - coefficients secretsharing.Polynomial - ciphersuite internal.Ciphersuite - maxSigners int - threshold int -} - -// NewParticipant instantiates a new participant with identifier id. -func NewParticipant(c internal.Ciphersuite, id *group.Scalar, maxSigners, threshold int) *Participant { - return &Participant{ - maxSigners: maxSigners, - threshold: threshold, - ciphersuite: c, - Identifier: id, - } -} - -func (p *Participant) challenge(id *group.Scalar, pubkey, r *group.Element) *group.Scalar { - // The paper actually hashes (id || dst || φ0 || r) - return p.ciphersuite.HDKG(internal.Concatenate( - id.Encode(), - pubkey.Encode(), - r.Encode(), - )) -} - -// Init returns a participant's output for the first round, and stores intermediate values internally. -func (p *Participant) Init() *Round1Data { - // Step 1 - secretCoefficients := secretsharing.NewPolynomial(uint(p.threshold)) - for i := 0; i < p.threshold; i++ { - secretCoefficients[i] = p.ciphersuite.Group.NewScalar().Random() - } - - // step 3 - we do this before step 2, so we can reuse the calculation of the commitment com[0] - com := secretsharing.Commit(p.ciphersuite.Group, secretCoefficients) - - // step 2 - k := p.ciphersuite.Group.NewScalar().Random() - r := p.ciphersuite.Group.Base().Multiply(k) - c := p.challenge(p.Identifier, com[0], r) - mu := k.Add(secretCoefficients[0].Copy().Multiply(c)) - - p.coefficients = secretCoefficients - p.publicShare = com[0] - - package1 := &Round1Data{ - SenderIdentifier: p.Identifier, - Commitment: com, - ProofOfKnowledge: frost.Signature{ - R: r, - Z: mu, - }, - } - - // step 4, broadcast package 1 to all other participants - return package1 -} - -// Continue ingests the broadcast data from other peers and returns a dedicated Round2Data structure for each peer. -func (p *Participant) Continue(r1data []*Round1Data) ([]*Round2Data, error) { - if len(r1data) != p.maxSigners { - return nil, errRound1DataElements - } - - r2data := make([]*Round2Data, 0, len(r1data)-1) - - for _, r1package := range r1data { - if r1package == nil { - continue - } - - peer := r1package.SenderIdentifier - if peer.Equal(p.Identifier) == 1 { - continue - } - - // round1, step 5 - c := p.challenge(peer, r1package.Commitment[0], r1package.ProofOfKnowledge.R) - rc := p.ciphersuite.Group.Base(). - Multiply(r1package.ProofOfKnowledge.Z). - Subtract(r1package.Commitment[0].Copy().Multiply(c)) - - if r1package.ProofOfKnowledge.R.Equal(rc) != 1 { - return nil, fmt.Errorf( - "%w: participant %v", - errInvalidSignature, - hex.EncodeToString(r1package.SenderIdentifier.Encode()), - ) - } - - // round 2, step 1 - fil := p.coefficients.Evaluate(p.ciphersuite.Group, peer) - r2data = append(r2data, &Round2Data{ - SenderIdentifier: p.Identifier.Copy(), - ReceiverIdentifier: peer.Copy(), - SecretShare: fil, - }) - } - - p.secretShare = p.coefficients.Evaluate(p.ciphersuite.Group, p.Identifier) - - return r2data, nil -} - -// Finalize ingests the broadcast data from round 1 and the round 2 data destined for the participant, -// and returns the participant's secret share and verification key, and the group's public key. -func (p *Participant) Finalize( - r1data []*Round1Data, - r2data []*Round2Data, -) (signingShare *group.Scalar, verificationShare, groupPublic *group.Element, err error) { - if len(r1data) != p.maxSigners { - return nil, nil, nil, errRound1DataElements - } - - if len(r1data) != len(r2data)+1 { - return nil, nil, nil, errRound2DataElements - } - - signingShare = p.ciphersuite.Group.NewScalar().Zero() - groupPublic = p.ciphersuite.Group.NewElement().Identity() - - for _, r2package := range r2data { - if r2package.ReceiverIdentifier.Equal(p.Identifier) != 1 { - return nil, nil, nil, errRound2InvalidReceiver - } - - // round 2, step 2 - - // Find the commitment from the participant. - var com []*group.Element - - for _, r1d := range r1data { - if r1d.SenderIdentifier.Equal(r2package.SenderIdentifier) == 1 { - com = r1d.Commitment - } - } - - if len(com) == 0 { - return nil, nil, nil, - fmt.Errorf("%w: %v", - errCommitmentNotFound, - hex.EncodeToString(r2package.SenderIdentifier.Encode())) - } - - // Verify the secret share is valid with regard to the commitment. - pk := p.ciphersuite.Group.Base().Multiply(r2package.SecretShare) - if !secretsharing.Verify(p.ciphersuite.Group, p.Identifier, pk, com) { - return nil, nil, nil, fmt.Errorf( - "%w: %v", - errInvalidSecretShare, - hex.EncodeToString(r2package.SenderIdentifier.Encode()), - ) - } - - // Round 2, step 3 - signingShare.Add(r2package.SecretShare) - - // Round 2, step 4 - groupPublic.Add(com[0]) - } - - signingShare.Add(p.secretShare) - groupPublic.Add(p.publicShare) - - // round 2, step 4 - verificationShare = p.ciphersuite.Group.Base().Multiply(signingShare) - - yi := ComputeVerificationShare(p.ciphersuite.Group, p.Identifier, r1data) - if verificationShare.Equal(yi) != 1 { - return nil, nil, nil, - fmt.Errorf("%w: want %q got %q", - errVerificationShareFailed, - hex.EncodeToString(yi.Encode()), - hex.EncodeToString(verificationShare.Encode()), - ) - } - - return signingShare, verificationShare, groupPublic, nil -} - -// ComputeVerificationShare computes the verification share for participant id given the commitments of round 1. -func ComputeVerificationShare(g group.Group, id *group.Scalar, r1data []*Round1Data) *group.Element { - yi := g.NewElement().Identity() - - for _, p := range r1data { - prime := g.NewElement().Identity() - one := g.NewScalar().One() - j := g.NewScalar().Zero() - - for _, com := range p.Commitment { - prime.Add(com.Copy().Multiply(id.Copy().Pow(j))) - j.Add(one) - } - - yi.Add(prime) - } - - return yi -} diff --git a/encoding.go b/encoding.go new file mode 100644 index 0000000..047f310 --- /dev/null +++ b/encoding.go @@ -0,0 +1,479 @@ +// SPDX-License-Identifier: MIT +// +// Copyright (C) 2024 Daniel Bourdrez. All Rights Reserved. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree or at +// https://spdx.org/licenses/MIT.html + +package frost + +import ( + "encoding/binary" + "encoding/hex" + "errors" + "fmt" + "slices" + + group "github.com/bytemare/crypto" + + "github.com/bytemare/frost/internal" + "github.com/bytemare/frost/keys" +) + +const ( + encConf byte = iota + 1 + encSigner + encSigShare + encSig + encPubKeyShare + encNonceCommitment + encLambda + encCommitment +) + +var ( + errInvalidConfigEncoding = errors.New( + "the threshold in the encoded configuration is higher than the number of maximum participants", + ) + errZeroIdentifier = errors.New("identifier cannot be 0") +) + +func encodedLength(encID byte, g group.Group, other ...uint64) uint64 { + eLen := uint64(g.ElementLength()) + sLen := uint64(g.ScalarLength()) + + switch encID { + case encConf: + return 1 + 3*8 + eLen + other[0] + case encSigner: + _ = other[3] + return other[0] + 2 + 2 + 2 + other[1] + other[2] + other[3] + case encSigShare: + return 1 + 8 + sLen + case encSig: + return eLen + sLen + case encPubKeyShare: + return 1 + 8 + 4 + eLen + other[0] + case encNonceCommitment: + return 8 + 2*sLen + encodedLength(encCommitment, g) + case encLambda: + return 32 + sLen + case encCommitment: + return 1 + 8 + 8 + 2*eLen + default: + panic("encoded id not recognized") + } +} + +// Encode serializes the Configuration into a compact byte slice. +func (c *Configuration) Encode() []byte { + g := group.Group(c.Ciphersuite) + pksLen := encodedLength(encPubKeyShare, g, c.Threshold*uint64(g.ElementLength())) + size := encodedLength(encConf, g, uint64(len(c.SignerPublicKeyShares))*pksLen) + out := make([]byte, 25, size) + out[0] = byte(g) + binary.LittleEndian.PutUint64(out[1:9], c.Threshold) + binary.LittleEndian.PutUint64(out[9:17], c.MaxSigners) + binary.LittleEndian.PutUint64(out[17:25], uint64(len(c.SignerPublicKeyShares))) + + out = append(out, c.GroupPublicKey.Encode()...) + + for _, pk := range c.SignerPublicKeyShares { + out = append(out, pk.Encode()...) + } + + return out +} + +type confHeader struct { + g group.Group + h, t, n, pksLen, nPks, length uint64 +} + +func (c *Configuration) decodeHeader(data []byte) (*confHeader, error) { + if len(data) <= 25 { + return nil, internal.ErrInvalidLength + } + + cs := Ciphersuite(data[0]) + if !cs.Available() { + return nil, internal.ErrInvalidCiphersuite + } + + g := group.Group(data[0]) + t := binary.LittleEndian.Uint64(data[1:9]) + n := binary.LittleEndian.Uint64(data[9:17]) + nPks := binary.LittleEndian.Uint64(data[17:25]) + pksLen := encodedLength(encPubKeyShare, g, t*uint64(g.ElementLength())) + length := encodedLength(encConf, g, nPks*pksLen) + + if t == 0 || t > n { + return nil, errInvalidConfigEncoding + } + + return &confHeader{ + g: g, + h: 25, + t: t, + n: n, + pksLen: pksLen, + nPks: nPks, + length: length, + }, nil +} + +func (c *Configuration) decode(header *confHeader, data []byte) error { + if uint64(len(data)) != header.length { + return internal.ErrInvalidLength + } + + gpk := header.g.NewElement() + if err := gpk.Decode(data[header.h : header.h+uint64(header.g.ElementLength())]); err != nil { + return fmt.Errorf("could not decode group public key: %w", err) + } + + offset := header.h + uint64(header.g.ElementLength()) + pks := make([]*keys.PublicKeyShare, header.nPks) + + conf := &Configuration{ + Ciphersuite: Ciphersuite(header.g), + Threshold: header.t, + MaxSigners: header.n, + GroupPublicKey: gpk, + SignerPublicKeyShares: pks, + group: header.g, + verified: false, + keysVerified: false, + } + + if err := conf.verifyConfiguration(); err != nil { + return err + } + + for j := range header.nPks { + pk := new(keys.PublicKeyShare) + if err := pk.Decode(data[offset : offset+header.pksLen]); err != nil { + return fmt.Errorf("could not decode signer public key share for signer %d: %w", j, err) + } + + offset += header.pksLen + pks[j] = pk + } + + if err := conf.verifySignerPublicKeyShares(); err != nil { + return err + } + + c.Ciphersuite = conf.Ciphersuite + c.Threshold = conf.Threshold + c.MaxSigners = conf.MaxSigners + c.GroupPublicKey = gpk + c.SignerPublicKeyShares = pks + c.group = group.Group(conf.Ciphersuite) + c.verified = true + c.keysVerified = true + + return nil +} + +// Decode deserializes the input data into the Configuration, or returns an error. +func (c *Configuration) Decode(data []byte) error { + header, err := c.decodeHeader(data) + if err != nil { + return err + } + + return c.decode(header, data) +} + +// Encode serializes the client with its long term values, containing its secret share. This is useful for saving state +// and backup. +func (s *Signer) Encode() []byte { + g := s.KeyShare.Group + keyShare := s.KeyShare.Encode() + nCommitments := len(s.NonceCommitments) + nLambdas := len(s.LambdaRegistry) + conf := s.Configuration.Encode() + outLength := encodedLength( + encSigner, + g, + uint64(len(conf)), + uint64(len(keyShare)), + uint64(nLambdas)*encodedLength(encLambda, g), + uint64(nCommitments)*encodedLength(encNonceCommitment, g), + ) + out := make([]byte, len(conf)+6, outLength) + + copy(out, conf) + binary.LittleEndian.PutUint16(out[len(conf):len(conf)+2], uint16(len(keyShare))) // key share length + binary.LittleEndian.PutUint16(out[len(conf)+2:len(conf)+4], uint16(nCommitments)) // number of commitments + binary.LittleEndian.PutUint16(out[len(conf)+4:len(conf)+6], uint16(nLambdas)) // number of lambda entries + + out = append(out, keyShare...) + + for k, v := range s.LambdaRegistry { + b, err := hex.DecodeString(k) + if err != nil { + panic("failed te revert hex encoding to bytes of " + k) + } + + out = append(out, b...) + out = append(out, v.Encode()...) + } + + for id, com := range s.NonceCommitments { + out = append(out, internal.Concatenate(internal.UInt64LE(id), + com.HidingNonce.Encode(), + com.BindingNonce.Encode(), + com.Commitment.Encode())...) + } + + return out +} + +func (n *Nonce) decode(g group.Group, id, comLen uint64, data []byte) error { + sLen := uint64(g.ScalarLength()) + offset := uint64(g.ScalarLength()) + + hn := g.NewScalar() + if err := hn.Decode(data[:offset]); err != nil { + return fmt.Errorf("can't decode hiding nonce for commitment %d: %w", id, err) + } + + bn := g.NewScalar() + if err := bn.Decode(data[offset : offset+sLen]); err != nil { + return fmt.Errorf("can't decode binding nonce for commitment %d: %w", id, err) + } + + offset += sLen + + com := new(Commitment) + if err := com.Decode(data[offset : offset+comLen]); err != nil { + return fmt.Errorf("can't decode nonce commitment %d: %w", id, err) + } + + n.HidingNonce = hn + n.BindingNonce = bn + n.Commitment = com + + return nil +} + +// Decode attempts to deserialize the encoded backup data into the Signer. +func (s *Signer) Decode(data []byte) error { + conf := new(Configuration) + + header, err := conf.decodeHeader(data) + if err != nil { + return err + } + + if err = conf.decode(header, data[:header.length]); err != nil { + return err + } + + if uint64(len(data)) <= header.length+6 { + return internal.ErrInvalidLength + } + + ksLen := uint64(binary.LittleEndian.Uint16(data[header.length : header.length+2])) + nCommitments := uint64(binary.LittleEndian.Uint16(data[header.length+2 : header.length+4])) + nLambdas := uint64(binary.LittleEndian.Uint16(data[header.length+4 : header.length+6])) + g := conf.group + nLen := encodedLength(encNonceCommitment, g) + lLem := encodedLength(encLambda, g) + + length := encodedLength(encSigner, g, header.length, ksLen, nCommitments*nLen, nLambdas*lLem) + if uint64(len(data)) != length { + return internal.ErrInvalidLength + } + + offset := header.length + 6 + + keyShare := new(keys.KeyShare) + if err = keyShare.Decode(data[offset : offset+ksLen]); err != nil { + return fmt.Errorf("failed to decode key share: %w", err) + } + + if err = conf.ValidateKeyShare(keyShare); err != nil { + return fmt.Errorf("invalid key share: %w", err) + } + + offset += ksLen + stop := offset + nLambdas*lLem + + lambdaRegistry := make(internal.LambdaRegistry, lLem) + if err = lambdaRegistry.Decode(g, data[offset:stop]); err != nil { + return fmt.Errorf("failed to decode lambda registry in signer: %w", err) + } + + offset = stop + commitments := make(map[uint64]*Nonce) + comLen := encodedLength(encCommitment, g) + nComLen := encodedLength(encNonceCommitment, g) + + for offset < uint64(len(data)) { + id := binary.LittleEndian.Uint64(data[offset : offset+8]) + + if _, exists := commitments[id]; exists { + return fmt.Errorf("multiple encoded commitments with the same id: %d", id) + } + + n := new(Nonce) + if err = n.decode(g, id, comLen, data[offset+8:]); err != nil { + return err + } + + commitments[id] = n + offset += nComLen + } + + s.KeyShare = keyShare + s.LambdaRegistry = lambdaRegistry + s.NonceCommitments = commitments + s.Configuration = conf + + return nil +} + +// Encode returns the serialized byte encoding of a participant's commitment. +func (c *Commitment) Encode() []byte { + hNonce := c.HidingNonceCommitment.Encode() + bNonce := c.BindingNonceCommitment.Encode() + + out := make([]byte, 17, encodedLength(encCommitment, c.Group)) + out[0] = byte(c.Group) + binary.LittleEndian.PutUint64(out[1:9], c.CommitmentID) + binary.LittleEndian.PutUint64(out[9:17], c.SignerID) + out = append(out, hNonce...) + out = append(out, bNonce...) + + return out +} + +// Decode attempts to deserialize the encoded commitment given as input, and to return it. +func (c *Commitment) Decode(data []byte) error { + if len(data) < 17 { + return errDecodeCommitmentLength + } + + g := group.Group(data[0]) + if !g.Available() { + return errInvalidCiphersuite + } + + if uint64(len(data)) != encodedLength(encCommitment, g) { + return errDecodeCommitmentLength + } + + cID := binary.LittleEndian.Uint64(data[1:9]) + + pID := binary.LittleEndian.Uint64(data[9:17]) + if pID == 0 { + return errZeroIdentifier + } + + offset := 17 + + hn := g.NewElement() + if err := hn.Decode(data[offset : offset+g.ElementLength()]); err != nil { + return fmt.Errorf("invalid encoding of hiding nonce commitment: %w", err) + } + + offset += g.ElementLength() + + bn := g.NewElement() + if err := bn.Decode(data[offset : offset+g.ElementLength()]); err != nil { + return fmt.Errorf("invalid encoding of binding nonce commitment: %w", err) + } + + c.Group = g + c.CommitmentID = cID + c.SignerID = pID + c.HidingNonceCommitment = hn + c.BindingNonceCommitment = bn + + return nil +} + +// Encode returns a compact byte encoding of the signature share. +func (s *SignatureShare) Encode() []byte { + share := s.SignatureShare.Encode() + + out := make([]byte, encodedLength(encSigShare, s.Group)) + out[0] = byte(s.Group) + binary.LittleEndian.PutUint64(out[1:9], s.SignerIdentifier) + copy(out[9:], share) + + return out +} + +// Decode takes a byte string and attempts to decode it to return the signature share. +func (s *SignatureShare) Decode(data []byte) error { + if len(data) < 1 { + return internal.ErrInvalidLength + } + + c := Ciphersuite(data[0]) + + g := c.ECGroup() + if g == 0 { + return internal.ErrInvalidCiphersuite + } + + if uint64(len(data)) != encodedLength(encSigShare, g) { + return internal.ErrInvalidLength + } + + id := binary.LittleEndian.Uint64(data[1:9]) + if id == 0 { + return errZeroIdentifier + } + + share := g.NewScalar() + if err := share.Decode(data[9:]); err != nil { + return fmt.Errorf("failed to decode signature share: %w", err) + } + + s.Group = g + s.SignerIdentifier = id + s.SignatureShare = share + + return nil +} + +// Encode serializes the signature into a byte string. +func (s *Signature) Encode() []byte { + r := s.R.Encode() + z := s.Z.Encode() + r = slices.Grow(r, len(z)) + + return append(r, z...) +} + +// Decode attempts to deserialize the encoded input into the signature in the group. +func (s *Signature) Decode(c Ciphersuite, data []byte) error { + g := c.ECGroup() + if g == 0 { + return internal.ErrInvalidCiphersuite + } + + eLen := g.ElementLength() + + if uint64(len(data)) != encodedLength(encSig, g) { + return internal.ErrInvalidLength + } + + s.R = g.NewElement() + if err := s.R.Decode(data[:eLen]); err != nil { + return fmt.Errorf("invalid signature - decoding R: %w", err) + } + + s.Z = g.NewScalar() + if err := s.Z.Decode(data[eLen:]); err != nil { + return fmt.Errorf("invalid signature - decoding Z: %w", err) + } + + return nil +} diff --git a/examples_test.go b/examples_test.go index b3b2807..d1a6bd1 100644 --- a/examples_test.go +++ b/examples_test.go @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT // -// Copyright (C) 2023 Daniel Bourdrez. All Rights Reserved. +// Copyright (C) 2024 Daniel Bourdrez. All Rights Reserved. // // This source code is licensed under the MIT license found in the // LICENSE file in the root directory of this source tree or at @@ -9,214 +9,217 @@ package frost_test import ( - "encoding/hex" "fmt" - group "github.com/bytemare/crypto" - "github.com/bytemare/frost" - "github.com/bytemare/frost/dkg" + "github.com/bytemare/frost/debug" + "github.com/bytemare/frost/keys" ) -var ( - participantGeneratedInDKG *frost.Participant - commitment *frost.Commitment - groupPublicKeyGeneratedInDKG *group.Element -) +// Example_signer shows the execution steps of a FROST participant. +func Example_signer() { + maxSigners := uint64(5) + threshold := uint64(3) + message := []byte("example message") + ciphersuite := frost.Default + + // We assume you already have a pool of participants with distinct non-zero identifiers in [1:maxSingers] + // and their signing share. + // This example uses a centralised trusted dealer, but it is strongly recommended to use distributed key generation, + // e.g. from github.com/bytemare/dkg, which is compatible with FROST. + secretKeyShares, groupPublicKey, _ := debug.TrustedDealerKeygen(ciphersuite, nil, threshold, maxSigners) + + // Since we used a centralised key generation, we only take the first key share for our participant. + participantSecretKeyShare := secretKeyShares[0] + + // At key generation, each participant must send their public key share to the coordinator, and the collection must + // be broadcast to every participant. + publicKeyShares := make([]*keys.PublicKeyShare, len(secretKeyShares)) + for i, sk := range secretKeyShares { + publicKeyShares[i] = sk.Public() + } -// Example_dkg shows the distributed key generation procedure that must be executed by each participant to build the secret key. -func Example_dkg() { - // Each participant must be set to use the same configuration. - maximumAmountOfParticipants := 1 - threshold := 1 - configuration := frost.Ristretto255.Configuration() - - // Step 1: Initialise your participant. Each participant must be given an identifier that MUST be unique among - // all participants. For this example, this participant will have id = 1. - participantIdentifier := configuration.IDFromInt(1) - dkgParticipant := dkg.NewParticipant( - configuration.Ciphersuite, - participantIdentifier, - maximumAmountOfParticipants, - threshold, - ) - - // Step 2: Call Init() on each participant. This will return data that must be broadcast to all other participants - // over a secure channel. - round1Data := dkgParticipant.Init() - if round1Data.SenderIdentifier.Equal(participantIdentifier) != 1 { - panic("this is just a test, and it failed") + // This is how to set up the Configuration for FROST, the same for every signer and the coordinator. + // Note that every configuration setup for a Signer needs the public key shares of all other signers participating + // in a signing session (at least for the Sign() step). + configuration := &frost.Configuration{ + Ciphersuite: ciphersuite, + Threshold: threshold, + MaxSigners: maxSigners, + GroupPublicKey: groupPublicKey, + SignerPublicKeyShares: publicKeyShares, } - // Step 3: First, collect all round1Data from all other participants. Then call Continue() on each participant - // providing them with the compiled data. - accumulatedRound1Data := make([]*dkg.Round1Data, 0, maximumAmountOfParticipants) - accumulatedRound1Data = append(accumulatedRound1Data, round1Data) + if err := configuration.Init(); err != nil { + panic(err) + } - // This will return a dedicated package for each other participant that must be sent to them over a secure channel. - // The intended receiver is specified in the returned data. - // We ignore the error for the demo, but execution MUST be aborted upon errors. - round2Data, err := dkgParticipant.Continue(accumulatedRound1Data) + // Instantiate the participant using its secret share. + // A participant (or Signer) can be backed up by serialization, and directly instantiated from that backup. + participant, err := configuration.Signer(participantSecretKeyShare) if err != nil { panic(err) - } else if len(round2Data) != len(accumulatedRound1Data)-1 { - panic("this is just a test, and it failed") } - // Step 3: First, collect all round2Data from all other participants. Then call Finalize() on each participant - // providing the same input as for Continue() and the collected data from the second round2. - accumulatedRound2Data := round2Data - - // This will, for each participant, return their secret key (which is a share of the global secret signing key), - // the corresponding verification key, and the global public key. - // We ignore the error for the demo, but execution MUST be aborted upon errors. - var participantsSecretKey *group.Scalar - participantsSecretKey, _, groupPublicKeyGeneratedInDKG, err = dkgParticipant.Finalize( - accumulatedRound1Data, - accumulatedRound2Data, - ) + // Step 1: call Commit() on each participant. This will return the participant's single-use commitment for a + // signature (which is independent of the future message to sign). + // Send this to the coordinator or all other participants (depending on your setup) over an authenticated + // channel (confidentiality is not required). + // A participant (or Signer) keeps an internal state during the protocol run across the two rounds. + // A participant can pre-compute multiple commitments in advance: these commitments can be shared, but the + // participant keeps an internal state of corresponding values, so it must the same instance or a backup of it using + // the serialization functions. + com := participant.Commit() + + // Step 2: collect the commitments from the other participants and coordinator-chosen message to sign, + // and finalize by signing the message. + commitments := make(frost.CommitmentList, threshold) + commitments[0] = com + + // This is not part of a participant's flow, but we need to collect the commitments of the other participants for + // the demo. + { + for i := uint64(1); i < threshold; i++ { + signer, err := configuration.Signer(secretKeyShares[i]) + if err != nil { + panic(err) + } + + commitments[i] = signer.Commit() + + } + } + + // Step 3: The participant receives the commitments from the other signers and the message to sign. + // Sign produces a signature share to be sent back to the coordinator. + // Execution MUST be aborted upon errors. + signatureShare, err := participant.Sign(message, commitments) if err != nil { panic(err) } - // It is important to set the group's public key. - configuration.GroupPublicKey = groupPublicKeyGeneratedInDKG - - // Now you can build a Signing Participant for the FROST protocol with this ID and key. - participantGeneratedInDKG = configuration.Participant(participantIdentifier, participantsSecretKey) + // This shows how to verify a single signature share + if err = configuration.VerifySignatureShare(signatureShare, message, commitments); err != nil { + panic(fmt.Sprintf("signature share verification failed: %s", err)) + } - fmt.Printf("Signing keys for participant set up. ID: %s\n", hex.EncodeToString(participantIdentifier.Encode())) + fmt.Println("Signing successful.") - // Output: Signing keys for participant set up. ID: 0100000000000000000000000000000000000000000000000000000000000000 + // Output: Signing successful. } -// Example_signer shows the execution steps of a FROST participant. -func Example_signer() { - // The following are your setup variables and configuration. - numberOfParticipants := 1 +// Example_coordinator shows how to aggregate signature shares produced by signers into the final signature +// and verify a final FROST signature. +func Example_coordinator() { + maxSigners := uint64(5) + threshold := uint64(3) + message := []byte("example message") + ciphersuite := frost.Default // We assume you already have a pool of participants with distinct non-zero identifiers and their signing share. - // See Example_dkg() on how to do generate these shares. - Example_dkg() - participant := participantGeneratedInDKG - - // Step 1: call Commit() on each participant. This will return the participant's single-use commitment. - // Send this to the coordinator or all other participants over an authenticated - // channel (confidentiality is not required). - // A participant keeps an internal state during the protocol run across the two rounds. - commitment = participant.Commit() - if commitment.Identifier.Equal(participant.KeyShare.Identifier) != 1 { - panic("this is just a test and it failed") + // The following block uses a centralised trusted dealer to do this, but it is strongly recommended to use + // distributed key generation, e.g. from github.com/bytemare/dkg, which is compatible with FROST. + secretKeyShares, groupPublicKey, _ := debug.TrustedDealerKeygen(ciphersuite, nil, threshold, maxSigners) + participantSecretKeyShares := secretKeyShares[:threshold] + participants := make([]*frost.Signer, threshold) + + // At key generation, each participant must send their public key share to the coordinator, and the collection must + // be broadcast to every participant. + publicKeyShares := make([]*keys.PublicKeyShare, len(secretKeyShares)) + for i, sk := range secretKeyShares { + publicKeyShares[i] = sk.Public() } - // Step 2: collect the commitments from the other participants and coordinator-chosen the message to sign, - // and finalize by signing the message. - message := []byte("example") - commitments := make(frost.CommitmentList, 0, numberOfParticipants) - commitments = append(commitments, commitment) - - // This will produce a signature share to be sent back to the coordinator. - // We ignore the error for the demo, but execution MUST be aborted upon errors. - signatureShare, _ := participant.Sign(message, commitments) - if !participant.VerifySignatureShare( - commitment, - participant.GroupPublicKey, - signatureShare.SignatureShare, - commitments, - message, - ) { - panic("this is a test and it failed") + // This is how to set up the Configuration for FROST, the same for every signer and the coordinator. + configuration := &frost.Configuration{ + Ciphersuite: ciphersuite, + Threshold: threshold, + MaxSigners: maxSigners, + GroupPublicKey: groupPublicKey, + SignerPublicKeyShares: publicKeyShares, } - fmt.Println("Signing successful.") + if err := configuration.Init(); err != nil { + panic(err) + } - // Output: Signing keys for participant set up. ID: 0100000000000000000000000000000000000000000000000000000000000000 - // Signing successful. -} + // Create a participant on each instance + for i, ks := range participantSecretKeyShares { + signer, err := configuration.Signer(ks) + if err != nil { + panic(err) + } -// Example_coordinator shows the execution steps of a FROST coordinator. -func Example_coordinator() { - /* - The Coordinator is an entity with the following responsibilities: - - 1. Determining which participants will participate (at least MIN_PARTICIPANTS in number); - 2. Coordinating rounds (receiving and forwarding inputs among participants); and - 3. Aggregating signature shares output by each participant, and publishing the resulting signature. - - Note that it is possible to deploy the protocol without a distinguished Coordinator. - */ - - // The following are your setup variables and configuration. - const ( - maxParticipants = 1 - numberOfParticipants = 1 // Must be >= to the threshold, and <= to the total number of participants. - ) - - // 0. We suppose a previous run of DKG with a setup of participants. Here we will only use 1 participant. - Example_dkg() - participant := participantGeneratedInDKG - groupPublicKey := groupPublicKeyGeneratedInDKG - - // A coordinator CAN be a participant. In this instance, we chose it not to be one. - configuration := frost.Ristretto255.Configuration(groupPublicKey) - coordinator := configuration.Participant(nil, nil) - - // 1. Determine which participants will participate (at least MIN_PARTICIPANTS in number). - //participantIdentifiers := [numberOfParticipants]*group.Scalar{ - // participant.KeyShare.Identifier, - //} - participantPublicKeys := []*group.Element{ - participant.ParticipantInfo.PublicKey, + participants[i] = signer } - // 2. Receive the participant's commitments and sort the list. Then send the message to be signed and the sorted - // received commitment list to each participant. - commitments := frost.CommitmentList{ - participant.Commit(), + // Pre-commit + commitments := make(frost.CommitmentList, threshold) + for i, p := range participants { + commitments[i] = p.Commit() } - message := []byte("example") commitments.Sort() - // 3. Collect the participants signature shares, and aggregate them to produce the final signature. This signature - // SHOULD be verified. - p1SignatureShare, _ := participant.Sign(message, commitments) - signatureShares := [numberOfParticipants]*frost.SignatureShare{ - p1SignatureShare, + // Sign + signatureShares := make([]*frost.SignatureShare, threshold) + for i, p := range participants { + var err error + signatureShares[i], err = p.Sign(message, commitments) + if err != nil { + panic(err) + } } - signature := coordinator.Aggregate(commitments, message, signatureShares[:]) + // Everything above was a simulation of commitment and signing rounds to produce the signature shares. + // The following shows how to aggregate these shares, and if verification fails, how to identify a misbehaving signer. - if !frost.Verify(configuration.Ciphersuite, message, signature, groupPublicKey) { - fmt.Println("invalid signature") + // The coordinator assembles the shares. If the verify argument is set to true, AggregateSignatures will internally + // verify each signature share and return an error on the first that is invalid. It will also verify whether the + // output signature is valid. + signature, err := configuration.AggregateSignatures(message, signatureShares, commitments, true) + if err != nil { + panic(err) + } + + // Verify the signature and identify potential foul players. Note that since we set verify to true when calling + // AggregateSignatures, the following is redundant. + // Anyone can verify the signature given the ciphersuite parameter, message, and the group public key. + if err = frost.VerifySignature(ciphersuite, message, signature, groupPublicKey); err != nil { // At this point one should try to identify which participant's signature share is invalid and act on it. // This verification is done as follows: - for i, signatureShare := range signatureShares { - // Verify whether we have the participants commitment - commitmentI := commitments.Get(signatureShare.Identifier) - if commitmentI == nil { - panic("commitment not found") - } - - // Get the public key corresponding to the signature share's participant - pki := participantPublicKeys[i-1] - - if !coordinator.VerifySignatureShare( - commitmentI, - pki, - signatureShare.SignatureShare, - commitments, - message, - ) { - fmt.Printf("participant %v produced an invalid signature share", signatureShare.Identifier.Encode()) + for _, signatureShare := range signatureShares { + if err := configuration.VerifySignatureShare(signatureShare, message, commitments); err != nil { + panic( + fmt.Sprintf( + "participant %v produced an invalid signature share: %s", + signatureShare.SignerIdentifier, + err, + ), + ) } } - panic("Failed.") + fmt.Println(err) + panic("Signature verification failed.") } - fmt.Printf("Valid signature for %q.", message) + fmt.Println("Signature is valid.") + + // Output: Signature is valid. +} + +func Example_key_generation() { +} + +func Example_existing_keys() { +} + +// Example_serialization shows how to encode and decode data used in FROST. +func Example_serialization() { + // Private keys and scalars. + + // Public keys and elements. - // Output: Signing keys for participant set up. ID: 0100000000000000000000000000000000000000000000000000000000000000 - // Valid signature for "example". + // Key shares } diff --git a/frost.go b/frost.go index ad4b635..db58e3d 100644 --- a/frost.go +++ b/frost.go @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT // -// Copyright (C) 2023 Daniel Bourdrez. All Rights Reserved. +// Copyright (C) 2024 Daniel Bourdrez. All Rights Reserved. // // This source code is licensed under the MIT license found in the // LICENSE file in the root directory of this source tree or at @@ -10,206 +10,417 @@ package frost import ( + "errors" + "fmt" + "math/big" + group "github.com/bytemare/crypto" - "github.com/bytemare/hash" secretsharing "github.com/bytemare/secret-sharing" "github.com/bytemare/frost/internal" + "github.com/bytemare/frost/keys" ) +/* +- check RFC +- update description + - more buzz + - show supported ciphersuites +- Check for + - FROST2-CKM: https://eprint.iacr.org/2021/1375 (has duplicate checks) + - FROST2-BTZ: https://eprint.iacr.org/2022/833 + - FROST3 (ROAST): https://eprint.iacr.org/2022/550 (most efficient variant of FROST) + - wrapper increasing robustness and apparently reducing some calculations? + - Chu: https://eprint.iacr.org/2023/899 + - re-randomize keys: https://eprint.iacr.org/2024/436.pdf + +TODO: + +- identifiers, min and max, are uint16 + +- verify serialize and deserialize functions of messages, scalars, and elements +- add deserialization examples with hardcoded input +- add versioning to encodings +- align on serialization of https://frost.zfnd.org/user/serialization.html (good doc)\ +- make a `go run`-able program to generate trusted dealer keys +- DKG: can derive an identifier from a byte string (e.g. name or email address) + +*/ + // Ciphersuite identifies the group and hash to use for FROST. type Ciphersuite byte const ( - // Ed25519 uses Edwards25519 and SHA-512, producing Ed25519-compliant signatures as specified in RFC8032. - Ed25519 Ciphersuite = 1 + iota + // Default and recommended ciphersuite for FROST. + Default = Ristretto255 - // Ristretto255 uses Ristretto255 and SHA-512. - Ristretto255 + // Ristretto255 uses Ristretto255 and SHA-512. This ciphersuite is recommended. + Ristretto255 = Ciphersuite(group.Ristretto255Sha512) // Ed448 uses Edwards448 and SHAKE256, producing Ed448-compliant signatures as specified in RFC8032. - ed448 + // ed448 = Ciphersuite(2). // P256 uses P-256 and SHA-256. - P256 + P256 = Ciphersuite(group.P256Sha256) - // Secp256k1 uses Secp256k1 and SHA-256. - Secp256k1 + // P384 uses P-384 and SHA-384. + P384 = Ciphersuite(group.P384Sha384) - ed25519ContextString = "FROST-ED25519-SHA512-v1" - ristretto255ContextString = "FROST-RISTRETTO255-SHA512-v1" - p256ContextString = "FROST-P256-SHA256-v1" - secp256k1ContextString = "FROST-secp256k1-SHA256-v1" + // P521 uses P-521 and SHA-512. + P521 = Ciphersuite(group.P521Sha512) - /* + // Ed25519 uses Edwards25519 and SHA-512, producing Ed25519-compliant signatures as specified in RFC8032. + Ed25519 = Ciphersuite(group.Edwards25519Sha512) - ed448ContextString = "FROST-ED448-SHAKE256-v1" - */ + // Secp256k1 uses Secp256k1 and SHA-256. + Secp256k1 = Ciphersuite(group.Secp256k1) ) // Available returns whether the selected ciphersuite is available. func (c Ciphersuite) Available() bool { switch c { - case Ed25519, Ristretto255, P256, Secp256k1: + case Ed25519, Ristretto255, P256, P384, P521, Secp256k1: return true - case ed448: - return false default: return false } } -// Configuration returns a configuration created for the ciphersuite. -func (c Ciphersuite) Configuration(groupPublicKey ...*group.Element) *Configuration { +// ECGroup returns the elliptic curve group used in the ciphersuite. +func (c Ciphersuite) ECGroup() group.Group { if !c.Available() { - return nil + return 0 } - var pk *group.Element - if len(groupPublicKey) != 0 { - pk = groupPublicKey[0] + return group.Group(c) +} + +// Configuration holds the Configuration for a signing session. +type Configuration struct { + GroupPublicKey *group.Element + SignerPublicKeyShares []*keys.PublicKeyShare + Threshold uint64 + MaxSigners uint64 + Ciphersuite Ciphersuite + group group.Group + verified bool + keysVerified bool +} + +var ( + errInvalidThresholdParameter = errors.New("threshold is 0 or higher than maxSigners") + errInvalidMaxSignersOrder = errors.New("maxSigners is higher than group order") + errInvalidNumberOfPublicKeys = errors.New("invalid number of public keys (lower than threshold or above maximum)") +) + +// Init verifies whether the configuration's components are valid, in which case it initializes internal values, or +// returns an error otherwise. +func (c *Configuration) Init() error { + if !c.verified { + if err := c.verifyConfiguration(); err != nil { + return err + } } - switch c { - case Ed25519: - return &Configuration{ - GroupPublicKey: pk, - Ciphersuite: internal.Ciphersuite{ - ContextString: []byte(ed25519ContextString), - Hash: hash.SHA512, - Group: group.Edwards25519Sha512, - }, + if !c.keysVerified { + if err := c.verifySignerPublicKeyShares(); err != nil { + return err } - case Ristretto255: - return &Configuration{ - GroupPublicKey: pk, - Ciphersuite: internal.Ciphersuite{ - Group: group.Ristretto255Sha512, - Hash: hash.SHA512, - ContextString: []byte(ristretto255ContextString), - }, + } + + return nil +} + +// Signer returns a new participant of the protocol instantiated from the Configuration and the signer's key share. +func (c *Configuration) Signer(keyShare *keys.KeyShare) (*Signer, error) { + if !c.verified || !c.keysVerified { + if err := c.Init(); err != nil { + return nil, err } - case P256: - return &Configuration{ - GroupPublicKey: pk, - Ciphersuite: internal.Ciphersuite{ - Group: group.P256Sha256, - Hash: hash.SHA256, - ContextString: []byte(p256ContextString), - }, + } + + if err := c.ValidateKeyShare(keyShare); err != nil { + return nil, err + } + + return &Signer{ + KeyShare: keyShare, + LambdaRegistry: make(internal.LambdaRegistry), + NonceCommitments: make(map[uint64]*Nonce), + HidingRandom: nil, + BindingRandom: nil, + Configuration: c, + }, nil +} + +// ValidatePublicKeyShare returns an error if they PublicKeyShare has invalid components or properties that not +// compatible with the configuration. +func (c *Configuration) ValidatePublicKeyShare(pks *keys.PublicKeyShare) error { + if !c.verified { + if err := c.verifyConfiguration(); err != nil { + return err } - case Secp256k1: - return &Configuration{ - GroupPublicKey: pk, - Ciphersuite: internal.Ciphersuite{ - ContextString: []byte(secp256k1ContextString), - Hash: hash.SHA256, - Group: group.Secp256k1, - }, + } + + if pks == nil { + return errors.New("public key share is nil") + } + + if pks.Group != c.group { + return fmt.Errorf("key share has invalid group parameter, want %s got %d", c.group, pks.Group) + } + + if err := c.validateIdentifier(pks.ID); err != nil { + return fmt.Errorf("invalid identifier for public key share, the %w", err) + } + + if err := c.validateGroupElement(pks.PublicKey); err != nil { + return fmt.Errorf("invalid public key for participant %d, the key %w", pks.ID, err) + } + + return nil +} + +// ValidateKeyShare returns an error if they KeyShare has invalid components or properties that not compatible with the +// configuration. +func (c *Configuration) ValidateKeyShare(keyShare *keys.KeyShare) error { + if !c.verified || !c.keysVerified { + if err := c.Init(); err != nil { + return err } - case ed448: - return nil - default: - return nil } + + if keyShare == nil { + return errors.New("provided key share is nil") + } + + if err := c.ValidatePublicKeyShare(keyShare.Public()); err != nil { + return err + } + + if c.GroupPublicKey.Equal(keyShare.GroupPublicKey) != 1 { + return errors.New( + "the key share's group public key does not match the one in the configuration", + ) + } + + if keyShare.Secret == nil || keyShare.Secret.IsZero() { + return errors.New("provided key share has invalid secret key") + } + + if c.group.Base().Multiply(keyShare.Secret).Equal(keyShare.PublicKey) != 1 { + return errors.New("provided key share has non-matching secret and public keys") + } + + pk := c.getSignerPubKey(keyShare.ID) + if pk == nil { + return errors.New("provided key share has no registered signer identifier in the configuration") + } + + if pk.Equal(keyShare.PublicKey) != 1 { + return errors.New( + "provided key share has a different public key than " + + "the one registered for that signer in the configuration", + ) + } + + return nil } -// Configuration holds long term configuration information. -type Configuration struct { - GroupPublicKey *group.Element - Ciphersuite internal.Ciphersuite +func (c *Configuration) verifySignerPublicKeyShares() error { + length := uint64(len(c.SignerPublicKeyShares)) + if length < c.Threshold || length > c.MaxSigners { + return errInvalidNumberOfPublicKeys + } + + // Sets to detect duplicates. + pkSet := make(map[string]uint64, len(c.SignerPublicKeyShares)) + idSet := make(map[uint64]struct{}, len(c.SignerPublicKeyShares)) + + for i, pks := range c.SignerPublicKeyShares { + if pks == nil { + return fmt.Errorf("empty public key share at index %d", i) + } + + if err := c.ValidatePublicKeyShare(pks); err != nil { + return err + } + + // Verify whether the ID has duplicates + if _, exists := idSet[pks.ID]; exists { + return fmt.Errorf("found duplicate identifier for signer %d", pks.ID) + } + + // Verify whether the public key has duplicates + s := string(pks.PublicKey.Encode()) + if id, exists := pkSet[s]; exists { + return fmt.Errorf("found duplicate public keys for signers %d and %d", pks.ID, id) + } + + pkSet[s] = pks.ID + idSet[pks.ID] = struct{}{} + } + + c.keysVerified = true + + return nil } -// IDFromInt returns a valid ID from and integer given the configuration. -func (c Configuration) IDFromInt(id int) *group.Scalar { - s := c.Ciphersuite.Group.NewScalar() +func (c *Configuration) verifyConfiguration() error { + if !c.Ciphersuite.Available() { + return internal.ErrInvalidCiphersuite + } - switch id { - case 0: - s.Zero() - case 1: - s.One() - default: - s = internal.IntegerToScalar(c.Ciphersuite.Group, id) + g := group.Group(c.Ciphersuite) + + if c.Threshold == 0 || c.Threshold > c.MaxSigners { + return errInvalidThresholdParameter + } + + order, _ := new(big.Int).SetString(g.Order(), 0) + if order == nil { + panic("can't set group order number") + } + + bigMax := new(big.Int).SetUint64(c.MaxSigners) + if order.Cmp(bigMax) != 1 { + // This is unlikely to happen, as the usual group orders cannot be represented in a uint64. + // Only a new, unregistered group would make it fail here. + return errInvalidMaxSignersOrder + } + + if err := c.validateGroupElement(c.GroupPublicKey); err != nil { + return fmt.Errorf("invalid group public key, the key %w", err) } - return s + c.group = g + c.verified = true + + return nil } -// Participant returns a new participant of the protocol instantiated from the configuration an input. -func (c Configuration) Participant(id, keyShare *group.Scalar) *Participant { - return &Participant{ - ParticipantInfo: ParticipantInfo{ - KeyShare: &secretsharing.KeyShare{ - Identifier: id, - SecretKey: keyShare, - }, - Lambda: nil, - PublicKey: c.Ciphersuite.Group.Base().Multiply(keyShare), - }, - Nonce: [2]*group.Scalar{}, - HidingRandom: nil, - BindingRandom: nil, - Configuration: c, +func (c *Configuration) getSignerPubKey(id uint64) *group.Element { + for _, pks := range c.SignerPublicKeyShares { + if pks.ID == id { + return pks.PublicKey + } } + + return nil } -// PublicKeys is the tuple defining a commitment. -type PublicKeys []*group.Element +func (c *Configuration) validateIdentifier(id uint64) error { + switch { + case id == 0: + return internal.ErrIdentifierIs0 + case id > c.MaxSigners: + return fmt.Errorf("identifier %d is above authorized range [1:%d]", id, c.MaxSigners) + } -// DeriveGroupInfo returns the group public key as well those from all participants. -func DeriveGroupInfo(g group.Group, max int, coms secretsharing.Commitment) (*group.Element, PublicKeys) { - pk := coms[0] - keys := make(PublicKeys, max) + return nil +} - for i := 0; i < max; i++ { - id := internal.IntegerToScalar(g, i) - pki := derivePublicPoint(g, coms, id) - keys[i] = pki +func (c *Configuration) validateGroupElement(e *group.Element) error { + switch { + case e == nil: + return errors.New("is nil") + case e.IsIdentity(): + return errors.New("is the identity element") + case group.Group(c.Ciphersuite).Base().Equal(e) == 1: + return errors.New("is the group generator (base element)") } - return pk, keys + return nil } -// TrustedDealerKeygen uses Shamir and Verifiable Secret Sharing to create secret shares of an input group secret. -// These shares should be distributed securely to relevant participants. Note that this is centralized and combines -// the shared secret at some point. To use a decentralized dealer-less key generation, use the dkg package. -func TrustedDealerKeygen( - g group.Group, - secret *group.Scalar, - max, min int, - coeffs ...*group.Scalar, -) ([]*secretsharing.KeyShare, *group.Element, secretsharing.Commitment, error) { - ss, err := secretsharing.New(g, uint(min)-1, coeffs...) - if err != nil { - return nil, nil, nil, err +func (c *Configuration) challenge(lambda *group.Scalar, message []byte, groupCommitment *group.Element) *group.Scalar { + chall := SchnorrChallenge(c.group, message, groupCommitment, c.GroupPublicKey) + return chall.Multiply(lambda) +} + +// SchnorrChallenge computes the per-message SchnorrChallenge. +func SchnorrChallenge(g group.Group, msg []byte, r, pk *group.Element) *group.Scalar { + return internal.H2(g, internal.Concatenate(r.Encode(), pk.Encode(), msg)) +} + +// VerifySignature returns whether the signature of the message is valid under publicKey. +func VerifySignature(c Ciphersuite, message []byte, signature *Signature, publicKey *group.Element) error { + g := c.ECGroup() + if g == 0 { + return internal.ErrInvalidCiphersuite } - privateKeyShares, coeffs, err := ss.Shard(secret, uint(max)) - if err != nil { - return nil, nil, nil, err + ch := SchnorrChallenge(g, message, signature.R, publicKey) + r := signature.R.Copy().Add(publicKey.Copy().Multiply(ch)) + l := g.Base().Multiply(signature.Z) + + // Clear the cofactor for Edwards25519. + if g == group.Edwards25519Sha512 { + cofactor := group.Edwards25519Sha512.NewScalar().SetUInt64(8) + l.Multiply(cofactor) + r.Multiply(cofactor) } - coms := secretsharing.Commit(g, coeffs) + if l.Equal(r) != 1 { + return errInvalidSignature + } - return privateKeyShares, coms[0], coms, nil + return nil } -func derivePublicPoint(g group.Group, coms secretsharing.Commitment, i *group.Scalar) *group.Element { - publicPoint := g.NewElement().Identity() - one := g.NewScalar().One() +// NewPublicKeyShare returns a PublicKeyShare from separately encoded key material. To deserialize a byte string +// produced by the PublicKeyShare.Encode() method, use the PublicKeyShare.Decode() method. +func NewPublicKeyShare(c Ciphersuite, id uint64, signerPublicKey []byte) (*keys.PublicKeyShare, error) { + if !c.Available() { + return nil, internal.ErrInvalidCiphersuite + } - j := g.NewScalar().Zero() - for _, com := range coms { - publicPoint.Add(com.Copy().Multiply(i.Copy().Pow(j))) - j.Add(one) + if id == 0 { + return nil, internal.ErrIdentifierIs0 } - return publicPoint + g := c.ECGroup() + + pk := g.NewElement() + if err := pk.Decode(signerPublicKey); err != nil { + return nil, fmt.Errorf("could not decode public share: %w", err) + } + + return &keys.PublicKeyShare{ + PublicKey: pk, + ID: id, + Group: g, + Commitment: nil, + }, nil } -// VerifyVSS allows verification of a participant's secret share given a VSS commitment to the secret polynomial. -func VerifyVSS(g group.Group, share *secretsharing.KeyShare, coms secretsharing.Commitment) bool { - pk := g.Base().Multiply(share.SecretKey) - return secretsharing.Verify(g, share.Identifier, pk, coms) +// NewKeyShare returns a KeyShare from separately encoded key material. To deserialize a byte string produced by the +// KeyShare.Encode() method, use the KeyShare.Decode() method. +func NewKeyShare( + c Ciphersuite, + id uint64, + secretShare, signerPublicKey, groupPublicKey []byte, +) (*keys.KeyShare, error) { + pks, err := NewPublicKeyShare(c, id, signerPublicKey) + if err != nil { + return nil, err + } + + g := c.ECGroup() + + s := g.NewScalar() + if err = s.Decode(secretShare); err != nil { + return nil, fmt.Errorf("could not decode secret share: %w", err) + } + + gpk := g.NewElement() + if err = gpk.Decode(groupPublicKey); err != nil { + return nil, fmt.Errorf("could not decode the group public key: %w", err) + } + + return &keys.KeyShare{ + Secret: s, + GroupPublicKey: gpk, + PublicKeyShare: secretsharing.PublicKeyShare(*pks), + }, nil } diff --git a/go.mod b/go.mod index 4580564..be5695d 100644 --- a/go.mod +++ b/go.mod @@ -1,19 +1,20 @@ module github.com/bytemare/frost -go 1.21 +go 1.22.3 require ( - filippo.io/edwards25519 v1.0.0 - github.com/bytemare/crypto v0.5.2 - github.com/bytemare/hash v0.1.5 - github.com/bytemare/secret-sharing v0.1.0 + filippo.io/edwards25519 v1.1.0 + github.com/bytemare/crypto v0.7.5 + github.com/bytemare/dkg v0.0.0-20240724114445-f93f2b4fc5d5 + github.com/bytemare/hash v0.3.0 + github.com/bytemare/secret-sharing v0.3.0 github.com/gtank/ristretto255 v0.1.2 ) require ( - filippo.io/nistec v0.0.2 // indirect - github.com/bytemare/hash2curve v0.2.2 // indirect - github.com/bytemare/secp256k1 v0.1.0 // indirect - golang.org/x/crypto v0.8.0 // indirect - golang.org/x/sys v0.7.0 // indirect + filippo.io/nistec v0.0.3 // indirect + github.com/bytemare/hash2curve v0.3.0 // indirect + github.com/bytemare/secp256k1 v0.1.4 // indirect + golang.org/x/crypto v0.26.0 // indirect + golang.org/x/sys v0.24.0 // indirect ) diff --git a/go.sum b/go.sum index 696a2d3..f6a5762 100644 --- a/go.sum +++ b/go.sum @@ -1,20 +1,22 @@ -filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek= -filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= -filippo.io/nistec v0.0.2 h1:/NIXTUimcHIh0E2DsYucHlICvUisgj28/XEnKSEptUs= -filippo.io/nistec v0.0.2/go.mod h1:84fxC9mi+MhC2AERXI4LSa8cmSVOzrFikg6hZ4IfCyw= -github.com/bytemare/crypto v0.5.2 h1:ogvfY5mmtrPc5Uhwq4mUEUDnTVig+UEF8gwnNAPaNbU= -github.com/bytemare/crypto v0.5.2/go.mod h1:kkx4ciRQFWcjMauezZo9SHw4YmqSTolWkfOVVTOXgAY= -github.com/bytemare/hash v0.1.5 h1:VW+X1YQ2b3chjRFHkRUnO42uclsQjXimdBCPOgIobR4= -github.com/bytemare/hash v0.1.5/go.mod h1:+QmWXTky/2b63ngqM5IYezGydn9UTFDhpX7mLYwYxCA= -github.com/bytemare/hash2curve v0.2.2 h1:zaGx6Z4/N4Pl9B7aGNtpbZ09vu1NNJGoJRRtHHl8oTw= -github.com/bytemare/hash2curve v0.2.2/go.mod h1:Wma3DmJdn8kqiK9j120hkWvC3tQVKS1PyA8ZzyG23BI= -github.com/bytemare/secp256k1 v0.1.0 h1:kjVJ06GAHSa+EJ7Rz1LdVgE0DQWdvUT77tmcGf7epXQ= -github.com/bytemare/secp256k1 v0.1.0/go.mod h1:hzquMsr3GXhVcqL9qFX7GGjmcT5dlQldKrArd7tcXHE= -github.com/bytemare/secret-sharing v0.1.0 h1:p+5TQw40/JnSMUgSe0OdvUACo9xZuiQDL/eLRPjPd50= -github.com/bytemare/secret-sharing v0.1.0/go.mod h1:NKTAXEhsxrVi2Yr5BCKFP9uhPWDtBStAPbyqLghFG4I= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +filippo.io/nistec v0.0.3 h1:h336Je2jRDZdBCLy2fLDUd9E2unG32JLwcJi0JQE9Cw= +filippo.io/nistec v0.0.3/go.mod h1:84fxC9mi+MhC2AERXI4LSa8cmSVOzrFikg6hZ4IfCyw= +github.com/bytemare/crypto v0.7.5 h1:aRZzSmRZFlPt4ydpI5KKr+UQbzNd1559mNnRj9tNjRw= +github.com/bytemare/crypto v0.7.5/go.mod h1:qRA6Tdg0Q9zMTuxeKkyVtEyDAgIuwM0YN5tffzFQJQw= +github.com/bytemare/dkg v0.0.0-20240724114445-f93f2b4fc5d5 h1:A19axQ2U11TMSRBYHsWUhRrudV8zR9HWQYcFAJozcaU= +github.com/bytemare/dkg v0.0.0-20240724114445-f93f2b4fc5d5/go.mod h1:jXow/Ycil51++OmvmBJ25ksb93RtSEnMxGxadZjWRVk= +github.com/bytemare/hash v0.3.0 h1:RqFMt3mqpF7UxLdjBrsOZm/2cz0cQiAOnYc9gDLopWE= +github.com/bytemare/hash v0.3.0/go.mod h1:YKOBchL0l8hRLFinVCL8YUKokGNIMhrWEHPHo3EV7/M= +github.com/bytemare/hash2curve v0.3.0 h1:41Npcbc+u/E252A5aCMtxDcz7JPkkX1QzShneTFm4eg= +github.com/bytemare/hash2curve v0.3.0/go.mod h1:itj45U8uqvCtWC0eCswIHVHswXcEHkpFui7gfJdPSfQ= +github.com/bytemare/secp256k1 v0.1.4 h1:6F1yP6RiUiWwH7AsGHsHktmHm24QcetdDcc39roBd2M= +github.com/bytemare/secp256k1 v0.1.4/go.mod h1:Pxb9miDs8PTt5mOktvvXiRflvLxI1wdxbXrc6IYsaho= +github.com/bytemare/secret-sharing v0.3.0 h1:IK+wi3dhh+s8amN4xqdpgd8Byi36jZJQ9oAX3bowto0= +github.com/bytemare/secret-sharing v0.3.0/go.mod h1:kZ8Ty314nPP1LLd9ZsAAoc77625CEvXzRtimtEE1M9I= github.com/gtank/ristretto255 v0.1.2 h1:JEqUCPA1NvLq5DwYtuzigd7ss8fwbYay9fi4/5uMzcc= github.com/gtank/ristretto255 v0.1.2/go.mod h1:Ph5OpO6c7xKUGROZfWVLiJf9icMDwUeIvY4OmlYW69o= -golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ= -golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= -golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/internal/binding.go b/internal/binding.go deleted file mode 100644 index bf51bcb..0000000 --- a/internal/binding.go +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-License-Identifier: MIT -// -// Copyright (C) 2023 Daniel Bourdrez. All Rights Reserved. -// -// This source code is licensed under the MIT license found in the -// LICENSE file in the root directory of this source tree or at -// https://spdx.org/licenses/MIT.html - -package internal - -import group "github.com/bytemare/crypto" - -// BindingFactor holds the binding factor scalar for the given identifier. -type BindingFactor struct { - Identifier *group.Scalar - BindingFactor *group.Scalar -} - -// BindingFactorList a list of BindingFactor. -type BindingFactorList []*BindingFactor - -// BindingFactorForParticipant returns the binding factor for a given participant identifier in the list. -func (b BindingFactorList) BindingFactorForParticipant(id *group.Scalar) *group.Scalar { - for _, bf := range b { - if id.Equal(bf.Identifier) == 1 { - return bf.BindingFactor - } - } - - panic("invalid participant") -} diff --git a/internal/ciphersuite.go b/internal/ciphersuite.go deleted file mode 100644 index 7784e5d..0000000 --- a/internal/ciphersuite.go +++ /dev/null @@ -1,97 +0,0 @@ -// SPDX-License-Identifier: MIT -// -// Copyright (C) 2023 Daniel Bourdrez. All Rights Reserved. -// -// This source code is licensed under the MIT license found in the -// LICENSE file in the root directory of this source tree or at -// https://spdx.org/licenses/MIT.html - -package internal - -import ( - "filippo.io/edwards25519" - group "github.com/bytemare/crypto" - "github.com/bytemare/hash" - "github.com/gtank/ristretto255" -) - -// Ciphersuite combines the group and hashing routines. -type Ciphersuite struct { - ContextString []byte - Hash hash.Hashing - Group group.Group -} - -func (c Ciphersuite) h1Ed25519(input []byte) *group.Scalar { - h := c.Hash.Hash(input) - - s := edwards25519.NewScalar() - if _, err := s.SetUniformBytes(h); err != nil { - panic(err) - } - - s2 := c.Group.NewScalar() - if err := s2.Decode(s.Bytes()); err != nil { - panic(err) - } - - return s2 -} - -func (c Ciphersuite) hx(input, dst []byte) *group.Scalar { - var sc *group.Scalar - - switch c.Group { - case group.Edwards25519Sha512: - sc = c.h1Ed25519(Concatenate(c.ContextString, dst, input)) - case group.Ristretto255Sha512: - h := c.Hash.Hash(c.ContextString, dst, input) - s := ristretto255.NewScalar().FromUniformBytes(h) - - sc = c.Group.NewScalar() - if err := sc.Decode(s.Encode(nil)); err != nil { - panic(err) - } - case group.P256Sha256, group.Secp256k1: - sc = c.Group.HashToScalar(input, append(c.ContextString, dst...)) - default: - panic(ErrInvalidParameters) - } - - return sc -} - -// H1 hashes the input and proves the "rho" DST. -func (c Ciphersuite) H1(input []byte) *group.Scalar { - return c.hx(input, []byte("rho")) -} - -// H2 hashes the input and proves the "chal" DST. -func (c Ciphersuite) H2(input []byte) *group.Scalar { - if c.Group == group.Edwards25519Sha512 { - // For compatibility with RFC8032 H2 doesn't use a domain separator. - return c.h1Ed25519(input) - } - - return c.hx(input, []byte("chal")) -} - -// H3 hashes the input and proves the "nonce" DST. -func (c Ciphersuite) H3(input []byte) *group.Scalar { - return c.hx(input, []byte("nonce")) -} - -// H4 hashes the input and proves the "msg" DST. -func (c Ciphersuite) H4(msg []byte) []byte { - return c.Hash.Hash(c.ContextString, []byte("msg"), msg) -} - -// H5 hashes the input and proves the "com" DST. -func (c Ciphersuite) H5(msg []byte) []byte { - return c.Hash.Hash(c.ContextString, []byte("com"), msg) -} - -// HDKG hashes the input to the "dkg" DST. -func (c Ciphersuite) HDKG(msg []byte) *group.Scalar { - return c.hx(msg, []byte("dkg")) -} diff --git a/internal/errors.go b/internal/errors.go new file mode 100644 index 0000000..babf0a9 --- /dev/null +++ b/internal/errors.go @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +// +// Copyright (C) 2024 Daniel Bourdrez. All Rights Reserved. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree or at +// https://spdx.org/licenses/MIT.html + +package internal + +import "errors" + +var ( + // ErrInvalidParameters indicates that wrong input has been provided. + ErrInvalidParameters = errors.New("invalid parameters") + + // ErrInvalidCiphersuite indicates a non-supported ciphersuite is being used. + ErrInvalidCiphersuite = errors.New("ciphersuite not available") + + // ErrInvalidLength indicates that a provided encoded data piece is not of the expected length. + ErrInvalidLength = errors.New("invalid encoding length") + + // ErrIdentifierIs0 is returned when the invalid 0 identifier is encountered. + ErrIdentifierIs0 = errors.New("identifier is 0") +) diff --git a/internal/hashing.go b/internal/hashing.go new file mode 100644 index 0000000..90e5652 --- /dev/null +++ b/internal/hashing.go @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: MIT +// +// Copyright (C) 2024 Daniel Bourdrez. All Rights Reserved. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree or at +// https://spdx.org/licenses/MIT.html + +package internal + +import ( + "filippo.io/edwards25519" + group "github.com/bytemare/crypto" + "github.com/bytemare/hash" + "github.com/gtank/ristretto255" +) + +const ( + ed25519ContextString = "FROST-ED25519-SHA512-v1" + ristretto255ContextString = "FROST-RISTRETTO255-SHA512-v1" + ed448ContextString = "FROST-ED448-SHAKE256-v1" + p256ContextString = "FROST-P256-SHA256-v1" + p384ContextString = "FROST-P384-SHA384-v1" + p521ContextString = "FROST-P521-SHA512-v1" + secp256k1ContextString = "FROST-secp256k1-SHA256-v1" +) + +type ciphersuite struct { + hash hash.Hasher + contextString []byte +} + +var ciphersuites = [group.Secp256k1 + 1]ciphersuite{ + { // Ristretto255 + hash: hash.SHA512.New(), + contextString: []byte(ristretto255ContextString), + }, + { // Ed448 - unused + hash: hash.SHAKE256.New(), + contextString: []byte(ed448ContextString), + }, + { // P256 + hash: hash.SHA256.New(), + contextString: []byte(p256ContextString), + }, + { // P384 + hash: hash.SHA384.New(), + contextString: []byte(p384ContextString), + }, + { // P521 + hash: hash.SHA512.New(), + contextString: []byte(p521ContextString), + }, + { // Ed25519 + hash: hash.SHA512.New(), + contextString: []byte(ed25519ContextString), + }, + { // Secp256k1 + hash: hash.SHA256.New(), + contextString: []byte(secp256k1ContextString), + }, +} + +func h1Ed25519(input ...[]byte) *group.Scalar { + hashed := ciphersuites[group.Edwards25519Sha512-1].hash.Hash(0, input...) + + s := edwards25519.NewScalar() + if _, err := s.SetUniformBytes(hashed); err != nil { + // Fails only if len(hashed) != 64, but the hash function above always returns 64 bytes. + panic(err) + } + + s2 := group.Edwards25519Sha512.NewScalar() + if err := s2.Decode(s.Bytes()); err != nil { + // Can't fail because the underlying encoding/decoding is compatible. + panic(err) + } + + return s2 +} + +func hx(g group.Group, input, dst []byte) *group.Scalar { + var sc *group.Scalar + c := ciphersuites[g-1] + + switch g { + case group.Edwards25519Sha512: + sc = h1Ed25519(c.contextString, dst, input) + case group.Ristretto255Sha512: + h := c.hash.Hash(0, c.contextString, dst, input) + s := ristretto255.NewScalar().FromUniformBytes(h) + + sc = g.NewScalar() + if err := sc.Decode(s.Encode(nil)); err != nil { + // Can't fail because the underlying encoding/decoding is compatible. + panic(err) + } + case group.P256Sha256, group.P384Sha384, group.P521Sha512, group.Secp256k1: + sc = g.HashToScalar(input, append(c.contextString, dst...)) + default: + // Can't fail because the function is always called with a compatible group previously checked. + panic(ErrInvalidParameters) + } + + return sc +} + +// H1 hashes the input and proves the "rho" DST. +func H1(g group.Group, input []byte) *group.Scalar { + return hx(g, input, []byte("rho")) +} + +// H2 hashes the input and proves the "chal" DST. +func H2(g group.Group, input []byte) *group.Scalar { + if g == group.Edwards25519Sha512 { + // For compatibility with RFC8032 H2 doesn't use a domain separator for Edwards25519. + return h1Ed25519(input) + } + + return hx(g, input, []byte("chal")) +} + +// H3 hashes the input and proves the "nonce" DST. +func H3(g group.Group, input []byte) *group.Scalar { + return hx(g, input, []byte("nonce")) +} + +// H4 hashes the input and proves the "msg" DST. +func H4(g group.Group, msg []byte) []byte { + cs := ciphersuites[g-1] + return cs.hash.Hash(0, cs.contextString, []byte("msg"), msg) +} + +// H5 hashes the input and proves the "com" DST. +func H5(g group.Group, msg []byte) []byte { + cs := ciphersuites[g-1] + return cs.hash.Hash(0, cs.contextString, []byte("com"), msg) +} diff --git a/internal/lambda.go b/internal/lambda.go new file mode 100644 index 0000000..02d9655 --- /dev/null +++ b/internal/lambda.go @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: MIT +// +// Copyright (C) 2024 Daniel Bourdrez. All Rights Reserved. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree or at +// https://spdx.org/licenses/MIT.html + +package internal + +import ( + "encoding/hex" + "fmt" + + group "github.com/bytemare/crypto" + "github.com/bytemare/hash" + secretsharing "github.com/bytemare/secret-sharing" +) + +// Lambda derives the interpolating value for id in the polynomial made by the participant identifiers. +// This function assumes that: +// - id is non-nil and != 0. +// - every scalar in participants is non-nil and != 0. +// - there are no duplicates in participants. +func Lambda(g group.Group, id uint64, participants []*group.Scalar) *group.Scalar { + sid := g.NewScalar().SetUInt64(id) + numerator := g.NewScalar().One() + denominator := g.NewScalar().One() + + for _, participant := range participants { + if participant.Equal(sid) == 1 { + continue + } + + numerator.Multiply(participant) + denominator.Multiply(participant.Copy().Subtract(sid)) + } + + return numerator.Multiply(denominator.Invert()) +} + +// LambdaRegistry holds a signers pre-computed lambda values, indexed by the list of participants they are associated +// to. A sorted set of participants will yield the same lambda. +type LambdaRegistry map[string]*group.Scalar + +const lambdaRegistryKeyDomainSeparator = "FROST-participants" + +func lambdaRegistryKey(participants []uint64) string { + a := fmt.Sprint(lambdaRegistryKeyDomainSeparator, participants) + return hex.EncodeToString(hash.SHA256.Hash([]byte(a))) // Length = 32 bytes, 64 in hex string +} + +// New creates a new lambda and for the participant list for the participant id, and registers it. +// This function assumes that: +// - id is non-nil and != 0. +// - every participant id is != 0. +// - there are no duplicates in participants. +func (l LambdaRegistry) New(g group.Group, id uint64, participants []uint64) *group.Scalar { + polynomial := secretsharing.NewPolynomialFromListFunc(g, participants, func(p uint64) *group.Scalar { + return g.NewScalar().SetUInt64(p) + }) + lambda := Lambda(g, id, polynomial) + l.Set(participants, lambda) + + return lambda +} + +// Get returns the recorded lambda for the list of participants, or nil if it wasn't found. +func (l LambdaRegistry) Get(participants []uint64) *group.Scalar { + key := lambdaRegistryKey(participants) + return l[key] +} + +// GetOrNew returns the recorded lambda for the list of participants, or created, records, and returns a new one if +// it wasn't found. +func (l LambdaRegistry) GetOrNew(g group.Group, id uint64, participants []uint64) *group.Scalar { + lambda := l.Get(participants) + if lambda == nil { + return l.New(g, id, participants) + } + + return lambda +} + +// Set records lambda for the given set of participants. +func (l LambdaRegistry) Set(participants []uint64, lambda *group.Scalar) { + key := lambdaRegistryKey(participants) + l[key] = lambda +} + +// Delete deletes the lambda for the given set of participants. +func (l LambdaRegistry) Delete(participants []uint64) { + key := lambdaRegistryKey(participants) + l[key].Zero() + delete(l, key) +} + +// Decode populates the receiver from the byte encoded serialization in data. +func (l LambdaRegistry) Decode(g group.Group, data []byte) error { + offset := 0 + for offset < len(data) { + key := data[offset : offset+32] + offset += 32 + + lambda := g.NewScalar() + if err := lambda.Decode(data[offset : offset+g.ScalarLength()]); err != nil { + return fmt.Errorf("failed to decode lambda: %w", err) + } + + l[hex.EncodeToString(key)] = lambda + offset += g.ScalarLength() + } + + return nil +} diff --git a/internal/utils.go b/internal/utils.go index 13e08c8..6f1b3f4 100644 --- a/internal/utils.go +++ b/internal/utils.go @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT // -// Copyright (C) 2023 Daniel Bourdrez. All Rights Reserved. +// Copyright (C) 2024 Daniel Bourdrez. All Rights Reserved. // // This source code is licensed under the MIT license found in the // LICENSE file in the root directory of this source tree or at @@ -11,32 +11,23 @@ package internal import ( cryptorand "crypto/rand" - "errors" + "encoding/binary" "fmt" - "math/big" - - group "github.com/bytemare/crypto" -) - -var ( - // ErrInvalidParameters indicates that wrong input has been provided. - ErrInvalidParameters = errors.New("invalid parameters") - - // ErrInvalidCiphersuite indicates a non-supported ciphersuite is being used. - ErrInvalidCiphersuite = errors.New("ciphersuite not available") - - // ErrInvalidParticipantBackup indicates the participant's encoded backup is not valid. - ErrInvalidParticipantBackup = errors.New("invalid backup") ) // Concatenate returns the concatenation of all bytes composing the input elements. func Concatenate(input ...[]byte) []byte { + if len(input) == 0 { + return []byte{} + } + if len(input) == 1 { if len(input[0]) == 0 { return nil } - return input[0] + // shallow clone + return append(input[0][:0:0], input[0]...) } length := 0 @@ -64,12 +55,10 @@ func RandomBytes(length int) []byte { return r } -// IntegerToScalar creates a group.Scalar given an int. -func IntegerToScalar(g group.Group, i int) *group.Scalar { - s := g.NewScalar() - if err := s.SetInt(big.NewInt(int64(i))); err != nil { - panic(err) - } +// UInt64LE returns the 8 byte little endian byte encoding of i. +func UInt64LE(i uint64) []byte { + out := [8]byte{} + binary.LittleEndian.PutUint64(out[:], i) - return s + return out[:] } diff --git a/keys/keys.go b/keys/keys.go new file mode 100644 index 0000000..440e80b --- /dev/null +++ b/keys/keys.go @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: MIT +// +// Copyright (C) 2024 Daniel Bourdrez. All Rights Reserved. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree or at +// https://spdx.org/licenses/MIT.html + +// Package keys defines the Key structures used in FROST. +package keys + +import ( + "fmt" + + group "github.com/bytemare/crypto" + "github.com/bytemare/dkg" + secretsharing "github.com/bytemare/secret-sharing" +) + +// KeyShare identifies the sharded key share for a given participant. +type KeyShare secretsharing.KeyShare + +// Identifier returns the identity for this share. +func (k *KeyShare) Identifier() uint64 { + return (*secretsharing.KeyShare)(k).Identifier() +} + +// SecretKey returns the participant's secret share. +func (k *KeyShare) SecretKey() *group.Scalar { + return (*secretsharing.KeyShare)(k).SecretKey() +} + +// Public returns the public key share and identifier corresponding to the secret key share. +func (k *KeyShare) Public() *PublicKeyShare { + return (*PublicKeyShare)(&k.PublicKeyShare) +} + +// Encode serializes k into a compact byte string. +func (k *KeyShare) Encode() []byte { + return (*secretsharing.KeyShare)(k).Encode() +} + +// Decode deserializes the compact encoding obtained from Encode(), or returns an error. +func (k *KeyShare) Decode(data []byte) error { + if err := (*secretsharing.KeyShare)(k).Decode(data); err != nil { + return fmt.Errorf("%w", err) + } + + return nil +} + +// UnmarshalJSON decodes data into k, or returns an error. +func (k *KeyShare) UnmarshalJSON(data []byte) error { + if err := (*secretsharing.KeyShare)(k).UnmarshalJSON(data); err != nil { + return fmt.Errorf("%w", err) + } + + return nil +} + +// PublicKeyShare specifies the public key of a participant identified with ID. +type PublicKeyShare secretsharing.PublicKeyShare + +// Verify returns whether the PublicKeyShare's public key is valid given its VSS commitment to the secret polynomial. +func (p *PublicKeyShare) Verify(commitments [][]*group.Element) bool { + return dkg.VerifyPublicKey(dkg.Ciphersuite(p.Group), p.ID, p.PublicKey, commitments) == nil +} + +// Encode serializes p into a compact byte string. +func (p *PublicKeyShare) Encode() []byte { + return (*secretsharing.PublicKeyShare)(p).Encode() +} + +// Decode deserializes the compact encoding obtained from Encode(), or returns an error. +func (p *PublicKeyShare) Decode(data []byte) error { + if err := (*secretsharing.PublicKeyShare)(p).Decode(data); err != nil { + return fmt.Errorf("%w", err) + } + + return nil +} + +// UnmarshalJSON decodes data into p, or returns an error. +func (p *PublicKeyShare) UnmarshalJSON(data []byte) error { + if err := (*secretsharing.PublicKeyShare)(p).UnmarshalJSON(data); err != nil { + return fmt.Errorf("%w", err) + } + + return nil +} diff --git a/participant.go b/participant.go deleted file mode 100644 index d1851e1..0000000 --- a/participant.go +++ /dev/null @@ -1,233 +0,0 @@ -// SPDX-License-Identifier: MIT -// -// Copyright (C) 2023 Daniel Bourdrez. All Rights Reserved. -// -// This source code is licensed under the MIT license found in the -// LICENSE file in the root directory of this source tree or at -// https://spdx.org/licenses/MIT.html - -package frost - -import ( - "errors" - "fmt" - - group "github.com/bytemare/crypto" - secretsharing "github.com/bytemare/secret-sharing" - - "github.com/bytemare/frost/internal" -) - -// Participant is a signer of a group. -type Participant struct { - ParticipantInfo - Nonce [2]*group.Scalar - HidingRandom []byte - BindingRandom []byte - Configuration -} - -var errDecodeSignatureShare = errors.New("failed to decode signature share: invalid length") - -// ParticipantInfo holds the participant specific long-term values. -type ParticipantInfo struct { - KeyShare *secretsharing.KeyShare - Lambda *group.Scalar // lamba can be computed once and reused across FROST signing operations - PublicKey *group.Element -} - -func (p *Participant) generateNonce(s *group.Scalar, random []byte) *group.Scalar { - if random == nil { - random = internal.RandomBytes(32) - } - - enc := s.Encode() - - return p.Ciphersuite.H3(internal.Concatenate(random, enc)) -} - -// Backup serializes the client with its long term values, containing its secret share. -func (p *Participant) Backup() []byte { - return internal.Concatenate(p.ParticipantInfo.KeyShare.Identifier.Encode(), - p.ParticipantInfo.KeyShare.SecretKey.Encode(), - p.ParticipantInfo.Lambda.Encode()) -} - -// RecoverParticipant attempts to deserialize the encoded backup into a Participant. -func RecoverParticipant(c Ciphersuite, backup []byte) (*Participant, error) { - if !c.Available() { - return nil, internal.ErrInvalidCiphersuite - } - - conf := c.Configuration() - - sLen := conf.Ciphersuite.Group.ScalarLength() - if len(backup) != 3*sLen { - return nil, internal.ErrInvalidParticipantBackup - } - - id := conf.Ciphersuite.Group.NewScalar() - if err := id.Decode(backup[:sLen]); err != nil { - return nil, fmt.Errorf("decoding identity: %w", err) - } - - share := conf.Ciphersuite.Group.NewScalar() - if err := share.Decode(backup[sLen : 2*sLen]); err != nil { - return nil, fmt.Errorf("decoding key share: %w", err) - } - - lambda := conf.Ciphersuite.Group.NewScalar() - if err := lambda.Decode(backup[2*sLen:]); err != nil { - return nil, fmt.Errorf("decoding lambda: %w", err) - } - - p := conf.Participant(id, share) - p.Lambda = lambda - p.PublicKey = conf.Ciphersuite.Group.Base().Multiply(share) - - return p, nil -} - -// Commit generates a participants nonce and commitment, to be used in the second FROST round. The nonce must be kept -// secret, and the commitment sent to the coordinator. -func (p *Participant) Commit() *Commitment { - p.Nonce[0] = p.generateNonce(p.ParticipantInfo.KeyShare.SecretKey, p.HidingRandom) - p.Nonce[1] = p.generateNonce(p.ParticipantInfo.KeyShare.SecretKey, p.BindingRandom) - - return &Commitment{ - Identifier: p.ParticipantInfo.KeyShare.Identifier.Copy(), - HidingNonce: p.Ciphersuite.Group.Base().Multiply(p.Nonce[0]), - BindingNonce: p.Ciphersuite.Group.Base().Multiply(p.Nonce[1]), - } -} - -// Sign produces a participant's signature share of the message msg. -// -// Each participant MUST validate the inputs before processing the Coordinator's request. -// In particular, the Signer MUST validate commitment_list, deserializing each group Element in the list using -// DeserializeElement from {{dep-pog}}. If deserialization fails, the Signer MUST abort the protocol. Moreover, -// each participant MUST ensure that its identifier and commitments (from the first round) appear in commitment_list. -func (p *Participant) Sign(msg []byte, list CommitmentList) (*SignatureShare, error) { - // Compute the binding factor(s) - bindingFactorList := p.computeBindingFactors(list, msg) - bindingFactor := bindingFactorList.BindingFactorForParticipant(p.KeyShare.Identifier) - - // Compute group commitment - groupCommitment := p.computeGroupCommitment(list, bindingFactorList) - - // Compute the interpolating value - participantList := secretsharing.Polynomial(list.Participants()) - - lambdaID, err := participantList.DeriveInterpolatingValue(p.Ciphersuite.Group, p.KeyShare.Identifier) - if err != nil { - return nil, err - } - - p.Lambda = lambdaID.Copy() - - // Compute per message challenge - challenge := challenge(p.Ciphersuite, groupCommitment, p.Configuration.GroupPublicKey, msg) - - // Compute the signature share - sigShare := p.Nonce[0].Add( - p.Nonce[1].Multiply(bindingFactor).Add(lambdaID.Multiply(p.KeyShare.SecretKey).Multiply(challenge)), - ).Copy() - - // Clean up values - p.Nonce[0].Zero() - p.Nonce[1].Zero() - - return &SignatureShare{ - Identifier: p.ParticipantInfo.KeyShare.Identifier.Copy(), - SignatureShare: sigShare, - }, nil -} - -// computeBindingFactors computes binding factors based on the participant commitment list and the message to be signed. -func (p *Participant) computeBindingFactors(l CommitmentList, message []byte) internal.BindingFactorList { - if !l.IsSorted() { - panic(nil) - } - - h := p.Configuration.Ciphersuite.H4(message) - encodedCommitHash := p.Configuration.Ciphersuite.H5(l.Encode()) - rhoInputPrefix := internal.Concatenate(p.GroupPublicKey.Encode(), h, encodedCommitHash) - - bindingFactorList := make(internal.BindingFactorList, len(l)) - - for i, commitment := range l { - rhoInput := internal.Concatenate(rhoInputPrefix, commitment.Identifier.Encode()) - bindingFactor := p.Configuration.Ciphersuite.H1(rhoInput) - - bindingFactorList[i] = &internal.BindingFactor{ - Identifier: commitment.Identifier, - BindingFactor: bindingFactor, - } - } - - return bindingFactorList -} - -// computeGroupCommitment creates the group commitment from a commitment list. -func (p *Participant) computeGroupCommitment(l CommitmentList, list internal.BindingFactorList) *group.Element { - if !l.IsSorted() { - panic(nil) - } - - gc := p.Configuration.Ciphersuite.Group.NewElement().Identity() - - for _, commitment := range l { - if commitment.HidingNonce.IsIdentity() || commitment.BindingNonce.IsIdentity() { - panic("identity commitment") - } - - factor := list.BindingFactorForParticipant(commitment.Identifier) - bindingNonce := commitment.BindingNonce.Copy().Multiply(factor) - gc.Add(commitment.HidingNonce).Add(bindingNonce) - } - - return gc -} - -// SignatureShare represents a participants signature share, specifying which participant it was produced by. -type SignatureShare struct { - Identifier *group.Scalar - SignatureShare *group.Scalar -} - -// Encode returns a compact byte encoding of the signature share. -func (s SignatureShare) Encode() []byte { - id := s.Identifier.Encode() - share := s.SignatureShare.Encode() - - out := make([]byte, len(id)+len(share)) - copy(out, id) - copy(out[len(id):], share) - - return out -} - -// DecodeSignatureShare takes a byte string and attempts to decode it to return the signature share. -func (c Configuration) DecodeSignatureShare(data []byte) (*SignatureShare, error) { - g := c.Ciphersuite.Group - scalarLength := g.ScalarLength() - - if len(data) != 2*scalarLength { - return nil, errDecodeSignatureShare - } - - s := &SignatureShare{ - Identifier: g.NewScalar(), - SignatureShare: g.NewScalar(), - } - - if err := s.Identifier.Decode(data[:scalarLength]); err != nil { - return nil, fmt.Errorf("failed to decode signature share identifier: %w", err) - } - - if err := s.SignatureShare.Decode(data[scalarLength:]); err != nil { - return nil, fmt.Errorf("failed to decode signature share: %w", err) - } - - return s, nil -} diff --git a/schnorr.go b/schnorr.go deleted file mode 100644 index 5a51af3..0000000 --- a/schnorr.go +++ /dev/null @@ -1,90 +0,0 @@ -// SPDX-License-Identifier: MIT -// -// Copyright (C) 2023 Daniel Bourdrez. All Rights Reserved. -// -// This source code is licensed under the MIT license found in the -// LICENSE file in the root directory of this source tree or at -// https://spdx.org/licenses/MIT.html - -package frost - -import ( - "fmt" - "math/big" - - group "github.com/bytemare/crypto" - - "github.com/bytemare/frost/internal" -) - -// Signature represent a Schnorr signature. -type Signature struct { - R *group.Element - Z *group.Scalar -} - -// Encode serializes the signature into a byte string. -func (s *Signature) Encode() []byte { - return append(s.R.Encode(), s.Z.Encode()...) -} - -// Decode attempts to deserialize the encoded input into the signature in the group. -func (s *Signature) Decode(g group.Group, encoded []byte) error { - eLen := g.ElementLength() - sLen := g.ScalarLength() - - if len(encoded) != eLen+sLen { - return internal.ErrInvalidParameters - } - - if err := s.R.Decode(encoded[:eLen]); err != nil { - return fmt.Errorf("invalid signature - decoding R: %w", err) - } - - if err := s.Z.Decode(encoded[eLen:]); err != nil { - return fmt.Errorf("invalid signature - decoding Z: %w", err) - } - - return nil -} - -// challenge computes the per-message challenge. -func challenge(cs internal.Ciphersuite, r, pk *group.Element, msg []byte) *group.Scalar { - return cs.H2(internal.Concatenate(r.Encode(), pk.Encode(), msg)) -} - -func computeZ(r, challenge, key *group.Scalar) *group.Scalar { - return r.Add(challenge.Multiply(key)) -} - -// Sign returns a Schnorr signature over the message msg with the full secret signing key (as opposed to a key share). -func Sign(cs internal.Ciphersuite, msg []byte, key *group.Scalar) *Signature { - r := cs.Group.NewScalar().Random() - R := cs.Group.Base().Multiply(r) - pk := cs.Group.Base().Multiply(key) - c := challenge(cs, R, pk, msg) - z := computeZ(r, c, key) - - return &Signature{ - R: R, - Z: z, - } -} - -// Verify returns whether the signature of the message msg is valid under the public key pk. -func Verify(cs internal.Ciphersuite, msg []byte, signature *Signature, pk *group.Element) bool { - c := challenge(cs, signature.R, pk, msg) - l := cs.Group.Base().Multiply(signature.Z) - r := signature.R.Add(pk.Copy().Multiply(c)) - - if cs.Group == group.Edwards25519Sha512 { - cofactor := group.Edwards25519Sha512.NewScalar() - if err := cofactor.SetInt(big.NewInt(8)); err != nil { - panic(err) - } - - return l.Multiply(cofactor).Equal(r.Multiply(cofactor)) == 1 - } - - return l.Equal(r) == 1 -} diff --git a/signer.go b/signer.go new file mode 100644 index 0000000..88a2182 --- /dev/null +++ b/signer.go @@ -0,0 +1,214 @@ +// SPDX-License-Identifier: MIT +// +// Copyright (C) 2024 Daniel Bourdrez. All Rights Reserved. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree or at +// https://spdx.org/licenses/MIT.html + +package frost + +import ( + "crypto/rand" + "encoding/binary" + "fmt" + + group "github.com/bytemare/crypto" + + "github.com/bytemare/frost/internal" + "github.com/bytemare/frost/keys" +) + +// SignatureShare represents a Signer's signature share and its identifier. +type SignatureShare struct { + SignatureShare *group.Scalar + SignerIdentifier uint64 + Group group.Group +} + +// Signer is a participant in a signing group. +type Signer struct { + // The KeyShare holds the signer's secret and public info, such as keys and identifier. + KeyShare *keys.KeyShare + + // LambdaRegistry records all interpolating values for the signers for different combinations of participant + // groups. Each group makes up a unique polynomial defined by the participants' identifiers. A value will be + // computed once for the first time a group is encountered, and kept across encodings and decodings of the signer, + // accelerating subsequent signatures within the same group of signers. + LambdaRegistry internal.LambdaRegistry + + // NonceCommitments maps Nonce and their NonceCommitments to their Commitment's identifier. + NonceCommitments map[uint64]*Nonce + + // Configuration is the core FROST setup configuration. + Configuration *Configuration + + // HidingRandom can be set to force the use its value for HidingNonce generation. This is only encouraged for vector + // reproduction, but should be left to nil in any production deployments. + HidingRandom []byte + + // HidingRandom can be set to force the use its value for HidingNonce generation. This is only encouraged for vector + // reproduction, but should be left to nil in any production deployments. + BindingRandom []byte +} + +// Nonce holds the signing nonces and their commitments. The Signer.Commit() method will generate and record a new nonce +// and return the Commitment to that nonce. That Commitment will be used in Signer.Sign() and the associated nonces to +// create a signature share. Note that nonces and their commitments are agnostic of the upcoming message to sign, and +// can therefore be pre-computed and the commitments shared before the signing session, saving a round-trip. +type Nonce struct { + HidingNonce *group.Scalar + BindingNonce *group.Scalar + *Commitment +} + +// ClearNonceCommitment zeroes-out the nonces and their commitments, and unregisters the nonce record. +func (s *Signer) ClearNonceCommitment(commitmentID uint64) { + if com := s.NonceCommitments[commitmentID]; com != nil { + com.HidingNonce.Zero() + com.BindingNonce.Zero() + com.HidingNonceCommitment.Identity() + com.BindingNonceCommitment.Identity() + delete(s.NonceCommitments, commitmentID) + } +} + +// Identifier returns the Signer's identifier. +func (s *Signer) Identifier() uint64 { + return s.KeyShare.ID +} + +func randomCommitmentID() uint64 { + buf := make([]byte, 8) + + if _, err := rand.Read(buf); err != nil { + panic(fmt.Errorf("FATAL: %w", err)) + } + + return binary.LittleEndian.Uint64(buf) +} + +func (s *Signer) generateNonce(secret *group.Scalar, random []byte) *group.Scalar { + if random == nil { + random = internal.RandomBytes(32) + } + + return internal.H3(s.Configuration.group, internal.Concatenate(random, secret.Encode())) +} + +func (s *Signer) genNonceID() uint64 { + cid := randomCommitmentID() + + // In the extremely rare and unlikely case the CSPRNG returns an already registered ID, we try again 128 times + // before failing. + for range 128 { + if _, exists := s.NonceCommitments[cid]; !exists { + return cid + } + + cid = randomCommitmentID() + } + + panic("FATAL: CSPRNG could not generate unique commitment identifiers over 128 iterations") +} + +// Commit generates a signer's nonces and commitment, to be used in the second FROST round. The internal nonce must +// be kept secret, and the returned commitment sent to the signature aggregator. +func (s *Signer) Commit() *Commitment { + cid := s.genNonceID() + hn := s.generateNonce(s.KeyShare.Secret, s.HidingRandom) + bn := s.generateNonce(s.KeyShare.Secret, s.BindingRandom) + com := &Commitment{ + Group: s.Configuration.group, + SignerID: s.KeyShare.ID, + CommitmentID: cid, + HidingNonceCommitment: s.Configuration.group.Base().Multiply(hn), + BindingNonceCommitment: s.Configuration.group.Base().Multiply(bn), + } + s.NonceCommitments[cid] = &Nonce{ + HidingNonce: hn, + BindingNonce: bn, + Commitment: com, + } + + return com.Copy() +} + +func (s *Signer) verifyNonces(com *Commitment) error { + nonces, ok := s.NonceCommitments[com.CommitmentID] + if !ok { + return fmt.Errorf( + "the commitment identifier %d for signer %d in the commitments is unknown to the signer", + com.CommitmentID, + s.KeyShare.ID, + ) + } + + if nonces.HidingNonceCommitment.Equal(com.HidingNonceCommitment) != 1 { + return fmt.Errorf("invalid hiding nonce in commitment list for signer %d", s.KeyShare.ID) + } + + if nonces.BindingNonceCommitment.Equal(com.BindingNonceCommitment) != 1 { + return fmt.Errorf("invalid binding nonce in commitment list for signer %d", s.KeyShare.ID) + } + + return nil +} + +// VerifyCommitmentList checks for the Commitment list integrity and the signer's commitment. This function must not +// return an error for Sign to succeed. +func (s *Signer) VerifyCommitmentList(commitments CommitmentList) error { + // Validate general consistency of the commitment list. + if err := s.Configuration.ValidateCommitmentList(commitments); err != nil { + return fmt.Errorf("invalid list of commitments: %w", err) + } + + // The signer's id must be among the commitments. + commitment := commitments.Get(s.KeyShare.ID) + if commitment == nil { + return fmt.Errorf("signer identifier %d not found in the commitment list", s.KeyShare.ID) + } + + // Check commitment values for the signer. + return s.verifyNonces(commitment) +} + +// Sign produces a participant's signature share of the message msg. The CommitmentList must contain a Commitment +// produced on a previous call to Commit(). Once the signature share with Sign() is produced, the internal commitment +// and nonces are cleared and another call to Sign() with the same Commitment will return an error. +func (s *Signer) Sign(message []byte, commitments CommitmentList) (*SignatureShare, error) { + commitments.Sort() + + if err := s.VerifyCommitmentList(commitments); err != nil { + return nil, err + } + + groupCommitment, bindingFactors := commitments.groupCommitmentAndBindingFactors( + s.Configuration.GroupPublicKey, + message, + ) + + participants := commitments.Participants() + lambda := s.LambdaRegistry.GetOrNew(s.Configuration.group, s.KeyShare.ID, participants) + lambdaChall := s.Configuration.challenge(lambda, message, groupCommitment) + + commitmentID := commitments.Get(s.KeyShare.ID).CommitmentID + com := s.NonceCommitments[commitmentID] + hidingNonce := com.HidingNonce.Copy() + bindingNonce := com.BindingNonce + + // Compute the signature share: h + b*f + l*s + bindingFactor := bindingFactors[s.KeyShare.ID] + sigShare := hidingNonce. + Add(bindingFactor.Multiply(bindingNonce). + Add(lambdaChall.Multiply(s.KeyShare.Secret))) + + // Clean up values + s.ClearNonceCommitment(commitmentID) + + return &SignatureShare{ + Group: s.Configuration.group, + SignerIdentifier: s.KeyShare.ID, + SignatureShare: sigShare, + }, nil +} diff --git a/tests/commitment_test.go b/tests/commitment_test.go new file mode 100644 index 0000000..0aabf48 --- /dev/null +++ b/tests/commitment_test.go @@ -0,0 +1,470 @@ +// SPDX-License-Identifier: MIT +// +// Copyright (C) 2024 Daniel Bourdrez. All Rights Reserved. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree or at +// https://spdx.org/licenses/MIT.html + +package frost_test + +import ( + "fmt" + "slices" + "strings" + "testing" + + "github.com/bytemare/frost" +) + +func TestCommitment_Validate_InvalidConfiguration(t *testing.T) { + expectedErrorPrefix := "invalid group public key, the key is nil" + tt := &tableTest{ + Ciphersuite: frost.Ristretto255, + threshold: 3, + maxSigners: 5, + } + configuration := &frost.Configuration{ + Ciphersuite: tt.Ciphersuite, + Threshold: tt.threshold, + MaxSigners: tt.maxSigners, + GroupPublicKey: nil, + SignerPublicKeyShares: nil, + } + + if err := configuration.ValidateCommitment(nil); err == nil || + !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } +} + +func TestCommitment_Validate_NilCommitment(t *testing.T) { + expectedErrorPrefix := "the commitment is nil" + tt := &tableTest{ + Ciphersuite: frost.Ristretto255, + threshold: 3, + maxSigners: 4, + } + configuration := makeConf(t, tt) + + if err := configuration.ValidateCommitment(nil); err == nil || + !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } +} + +func TestCommitment_Validate_SignerIDs0(t *testing.T) { + tt := &tableTest{ + Ciphersuite: frost.Ristretto255, + threshold: 3, + maxSigners: 4, + } + configuration, signers := fullSetup(t, tt) + commitment := signers[0].Commit() + commitment.SignerID = 0 + expectedErrorPrefix := fmt.Sprintf( + "invalid identifier for signer in commitment %d, the identifier is 0", + commitment.CommitmentID, + ) + + if err := configuration.ValidateCommitment(commitment); err == nil || + !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } +} + +func TestCommitment_Validate_SignerIDInvalid(t *testing.T) { + tt := &tableTest{ + Ciphersuite: frost.Ristretto255, + threshold: 3, + maxSigners: 4, + } + configuration, signers := fullSetup(t, tt) + commitment := signers[0].Commit() + commitment.SignerID = tt.maxSigners + 1 + expectedErrorPrefix := fmt.Sprintf( + "invalid identifier for signer in commitment %d, the identifier %d is above authorized range [1:%d]", + commitment.CommitmentID, + commitment.SignerID, + tt.maxSigners, + ) + + if err := configuration.ValidateCommitment(commitment); err == nil || + !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } +} + +func TestCommitment_Validate_WrongGroup(t *testing.T) { + tt := &tableTest{ + Ciphersuite: frost.Ristretto255, + threshold: 2, + maxSigners: 3, + } + configuration, signers := fullSetup(t, tt) + com := signers[0].Commit() + com.Group = 2 + expectedErrorPrefix := fmt.Sprintf( + "commitment %d for participant 1 has an unexpected ciphersuite: expected ristretto255_XMD:SHA-512_R255MAP_RO_, got 2", + com.CommitmentID, + ) + + if err := configuration.ValidateCommitment(com); err == nil || + !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } +} + +func TestCommitment_Validate_BadHidingNonceCommitment(t *testing.T) { + tt := &tableTest{ + Ciphersuite: frost.Ristretto255, + threshold: 2, + maxSigners: 3, + } + configuration, signers := fullSetup(t, tt) + com := signers[0].Commit() + + // nil + expectedErrorPrefix := fmt.Sprintf( + "invalid commitment %d for signer %d, the hiding nonce commitment is nil", + com.CommitmentID, + com.SignerID, + ) + + com.HidingNonceCommitment = nil + if err := configuration.ValidateCommitment(com); err == nil || + !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } + + // point at infinity + expectedErrorPrefix = fmt.Sprintf( + "invalid commitment %d for signer %d, the hiding nonce commitment is the identity element", + com.CommitmentID, + com.SignerID, + ) + + com.HidingNonceCommitment = tt.ECGroup().NewElement() + if err := configuration.ValidateCommitment(com); err == nil || + !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } + + // generator + expectedErrorPrefix = fmt.Sprintf( + "invalid commitment %d for signer %d, the hiding nonce commitment is the group generator (base element)", + com.CommitmentID, + com.SignerID, + ) + + com.HidingNonceCommitment.Base() + if err := configuration.ValidateCommitment(com); err == nil || + !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } +} + +func TestCommitment_Validate_BadBindingNonceCommitment(t *testing.T) { + tt := &tableTest{ + Ciphersuite: frost.Ristretto255, + threshold: 2, + maxSigners: 3, + } + configuration, signers := fullSetup(t, tt) + com := signers[0].Commit() + + // nil + expectedErrorPrefix := fmt.Sprintf( + "invalid commitment %d for signer %d, the binding nonce commitment is nil", + com.CommitmentID, + com.SignerID, + ) + + com.BindingNonceCommitment = nil + if err := configuration.ValidateCommitment(com); err == nil || + !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } + + // point at infinity + expectedErrorPrefix = fmt.Sprintf( + "invalid commitment %d for signer %d, the binding nonce commitment is the identity element", + com.CommitmentID, + com.SignerID, + ) + + com.BindingNonceCommitment = tt.ECGroup().NewElement() + if err := configuration.ValidateCommitment(com); err == nil || + !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } + + // generator + expectedErrorPrefix = fmt.Sprintf( + "invalid commitment %d for signer %d, the binding nonce commitment is the group generator (base element)", + com.CommitmentID, + com.SignerID, + ) + + com.BindingNonceCommitment.Base() + if err := configuration.ValidateCommitment(com); err == nil || + !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } +} + +func TestCommitmentList_Sort(t *testing.T) { + testAll(t, func(t *testing.T, test *tableTest) { + signers := makeSigners(t, test) + coms := make(frost.CommitmentList, len(signers)) + + // signer A < signer B + coms[0] = signers[0].Commit() + coms[1] = signers[1].Commit() + coms[2] = signers[2].Commit() + + coms.Sort() + + if !coms.IsSorted() { + t.Fatal("expected sorted") + } + + // signer B > singer A + coms[0] = signers[1].Commit() + coms[1] = signers[0].Commit() + + coms.Sort() + + if !coms.IsSorted() { + t.Fatal("expected sorted") + } + + // signer B > singer A + coms[0] = signers[0].Commit() + coms[1] = signers[2].Commit() + coms[2] = signers[2].Commit() + + coms.Sort() + + if !coms.IsSorted() { + t.Fatal("expected sorted") + } + }) +} + +func TestCommitmentList_Validate_InvalidConfiguration(t *testing.T) { + expectedErrorPrefix := "invalid group public key, the key is nil" + tt := &tableTest{ + Ciphersuite: frost.Ristretto255, + threshold: 3, + maxSigners: 5, + } + configuration := &frost.Configuration{ + Ciphersuite: tt.Ciphersuite, + Threshold: tt.threshold, + MaxSigners: tt.maxSigners, + GroupPublicKey: nil, + SignerPublicKeyShares: nil, + } + + if err := configuration.ValidateCommitmentList(nil); err == nil || + !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } +} + +func TestCommitmentList_Validate_NoCommitments(t *testing.T) { + expectedErrorPrefix := "commitment list is empty" + tt := &tableTest{ + Ciphersuite: frost.Ristretto255, + threshold: 2, + maxSigners: 3, + } + configuration, signers := fullSetup(t, tt) + coms := make(frost.CommitmentList, len(signers)) + + for i, s := range signers { + coms[i] = s.Commit() + } + + if err := configuration.ValidateCommitmentList(nil); err == nil || + !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } + + if err := configuration.ValidateCommitmentList(frost.CommitmentList{}); err == nil || + !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } +} + +func TestCommitmentList_Validate_InsufficientCommitments(t *testing.T) { + expectedErrorPrefix := "too few commitments: expected at least 2 but got 1" + tt := &tableTest{ + Ciphersuite: frost.Ristretto255, + threshold: 2, + maxSigners: 3, + } + configuration, signers := fullSetup(t, tt) + coms := make(frost.CommitmentList, len(signers)) + + for i, s := range signers { + coms[i] = s.Commit() + } + + if err := configuration.ValidateCommitmentList(coms[:tt.threshold-1]); err == nil || + !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } +} + +func TestCommitmentList_Validate_TooManyCommitments(t *testing.T) { + expectedErrorPrefix := "too many commitments: expected 3 or less but got 4" + tt := &tableTest{ + Ciphersuite: frost.Ristretto255, + threshold: 2, + maxSigners: 3, + } + configuration, signers := fullSetup(t, tt) + coms := make(frost.CommitmentList, len(signers)+1) + + for i, s := range signers { + coms[i] = s.Commit() + } + coms[len(signers)] = coms[0].Copy() + + if err := configuration.ValidateCommitmentList(coms); err == nil || + !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } +} + +func TestCommitmentList_Validate_DuplicateSignerIDs(t *testing.T) { + expectedErrorPrefix := "commitment list contains multiple commitments of participant 2" + tt := &tableTest{ + Ciphersuite: frost.Ristretto255, + threshold: 3, + maxSigners: 4, + } + configuration, signers := fullSetup(t, tt) + coms := make(frost.CommitmentList, len(signers)) + + for i, s := range signers { + coms[i] = s.Commit() + } + + coms[2].SignerID = coms[1].SignerID + + if err := configuration.ValidateCommitmentList(coms); err == nil || + !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } +} + +func TestCommitmentList_Validate_InvalidCommitment(t *testing.T) { + tt := &tableTest{ + Ciphersuite: frost.Ristretto255, + threshold: 3, + maxSigners: 4, + } + configuration, signers := fullSetup(t, tt) + coms := make(frost.CommitmentList, len(signers)) + + for i, s := range signers { + coms[i] = s.Commit() + } + + coms[2].BindingNonceCommitment.Base() + expectedErrorPrefix := fmt.Sprintf( + "invalid commitment %d for signer %d, the binding nonce commitment is the group generator (base element)", + coms[2].CommitmentID, + coms[2].SignerID, + ) + + if err := configuration.ValidateCommitmentList(coms); err == nil || + !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } +} + +func TestCommitmentList_Validate_NilCommitment(t *testing.T) { + expectedErrorPrefix := "the commitment list has a nil commitment" + tt := &tableTest{ + Ciphersuite: frost.Ristretto255, + threshold: 3, + maxSigners: 4, + } + configuration, signers := fullSetup(t, tt) + coms := make(frost.CommitmentList, len(signers)) + + for i, s := range signers { + coms[i] = s.Commit() + } + + coms[2] = nil + + if err := configuration.ValidateCommitmentList(coms); err == nil || + !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } +} + +func TestCommitmentList_Validate_NotSorted(t *testing.T) { + expectedErrorPrefix := "commitment list is not sorted by signer identifiers" + tt := &tableTest{ + Ciphersuite: frost.Ristretto255, + threshold: 3, + maxSigners: 4, + } + configuration, signers := fullSetup(t, tt) + coms := make(frost.CommitmentList, len(signers)) + + for i, s := range signers { + coms[i] = s.Commit() + } + + coms[1].SignerID, coms[2].SignerID = coms[2].SignerID, coms[1].SignerID + + if err := configuration.ValidateCommitmentList(coms); err == nil || + !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } +} + +func TestCommitmentList_Validate_UnregisteredKey(t *testing.T) { + tt := &tableTest{ + Ciphersuite: frost.Ristretto255, + threshold: 3, + maxSigners: 4, + } + configuration, signers := fullSetup(t, tt) + coms := make(frost.CommitmentList, len(signers)) + + for i, s := range signers { + coms[i] = s.Commit() + } + + configuration.SignerPublicKeyShares = slices.Delete(configuration.SignerPublicKeyShares, 1, 2) + expectedErrorPrefix := fmt.Sprintf( + "signer identifier %d for commitment %d is not registered in the configuration", + coms[1].SignerID, + coms[1].CommitmentID, + ) + + if err := configuration.ValidateCommitmentList(coms); err == nil || + !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } +} + +func TestCommitmentList_ParticipantsScalar_Empty(t *testing.T) { + com := frost.CommitmentList{} + if out := com.ParticipantsScalar(); out != nil { + t.Fatal("unexpected output") + } + + com = frost.CommitmentList{nil, nil} + if out := com.ParticipantsScalar(); out != nil { + t.Fatal("unexpected output") + } +} diff --git a/tests/configuration_test.go b/tests/configuration_test.go new file mode 100644 index 0000000..a78cc80 --- /dev/null +++ b/tests/configuration_test.go @@ -0,0 +1,1051 @@ +// SPDX-License-Identifier: MIT +// +// Copyright (C) 2024 Daniel Bourdrez. All Rights Reserved. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree or at +// https://spdx.org/licenses/MIT.html + +package frost_test + +import ( + "fmt" + "slices" + "strings" + "testing" + + secretsharing "github.com/bytemare/secret-sharing" + + "github.com/bytemare/frost" + "github.com/bytemare/frost/debug" + "github.com/bytemare/frost/internal" + "github.com/bytemare/frost/keys" +) + +func TestConfiguration_Verify_InvalidCiphersuite(t *testing.T) { + expectedErrorPrefix := internal.ErrInvalidCiphersuite + + testAll(t, func(t *testing.T, test *tableTest) { + keyShares, groupPublicKey, _ := debug.TrustedDealerKeygen( + test.Ciphersuite, + nil, + test.threshold, + test.maxSigners, + ) + publicKeyShares := getPublicKeyShares(keyShares) + + configuration := &frost.Configuration{ + Ciphersuite: 2, + Threshold: test.threshold, + MaxSigners: test.maxSigners, + GroupPublicKey: groupPublicKey, + SignerPublicKeyShares: publicKeyShares, + } + + if err := configuration.Init(); err == nil || !strings.HasPrefix(err.Error(), expectedErrorPrefix.Error()) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } + }) +} + +func TestConfiguration_Verify_Threshold_0(t *testing.T) { + expectedErrorPrefix := "threshold is 0 or higher than maxSigners" + + testAll(t, func(t *testing.T, test *tableTest) { + keyShares, groupPublicKey, _ := debug.TrustedDealerKeygen( + test.Ciphersuite, + nil, + test.threshold, + test.maxSigners, + ) + publicKeyShares := getPublicKeyShares(keyShares) + + configuration := &frost.Configuration{ + Ciphersuite: test.Ciphersuite, + Threshold: 0, + MaxSigners: test.maxSigners, + GroupPublicKey: groupPublicKey, + SignerPublicKeyShares: publicKeyShares, + } + + if err := configuration.Init(); err == nil || !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } + }) +} + +func TestConfiguration_Verify_Threshold_Max(t *testing.T) { + expectedErrorPrefix := "threshold is 0 or higher than maxSigners" + + testAll(t, func(t *testing.T, test *tableTest) { + keyShares, groupPublicKey, _ := debug.TrustedDealerKeygen( + test.Ciphersuite, + nil, + test.threshold, + test.maxSigners, + ) + publicKeyShares := getPublicKeyShares(keyShares) + + configuration := &frost.Configuration{ + Ciphersuite: test.Ciphersuite, + Threshold: test.maxSigners + 1, + MaxSigners: test.maxSigners, + GroupPublicKey: groupPublicKey, + SignerPublicKeyShares: publicKeyShares, + } + + if err := configuration.Init(); err == nil || !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } + }) +} + +func TestConfiguration_Verify_GroupPublicKey_Nil(t *testing.T) { + expectedErrorPrefix := "invalid group public key, the key is nil" + + testAll(t, func(t *testing.T, test *tableTest) { + keyShares, _, _ := debug.TrustedDealerKeygen(test.Ciphersuite, nil, test.threshold, test.maxSigners) + publicKeyShares := getPublicKeyShares(keyShares) + + configuration := &frost.Configuration{ + Ciphersuite: test.Ciphersuite, + Threshold: test.threshold, + MaxSigners: test.maxSigners, + GroupPublicKey: nil, + SignerPublicKeyShares: publicKeyShares, + } + + if err := configuration.Init(); err == nil || !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } + }) +} + +func TestConfiguration_Verify_GroupPublicKey_Identity(t *testing.T) { + expectedErrorPrefix := "invalid group public key, the key is the identity element" + + testAll(t, func(t *testing.T, test *tableTest) { + keyShares, _, _ := debug.TrustedDealerKeygen(test.Ciphersuite, nil, test.threshold, test.maxSigners) + publicKeyShares := getPublicKeyShares(keyShares) + + configuration := &frost.Configuration{ + Ciphersuite: test.Ciphersuite, + Threshold: test.threshold, + MaxSigners: test.maxSigners, + GroupPublicKey: test.ECGroup().NewElement(), + SignerPublicKeyShares: publicKeyShares, + } + + if err := configuration.Init(); err == nil || !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } + }) +} + +func TestConfiguration_Verify_GroupPublicKey_Generator(t *testing.T) { + expectedErrorPrefix := "invalid group public key, the key is the group generator (base element)" + + testAll(t, func(t *testing.T, test *tableTest) { + keyShares, _, _ := debug.TrustedDealerKeygen(test.Ciphersuite, nil, test.threshold, test.maxSigners) + publicKeyShares := getPublicKeyShares(keyShares) + + configuration := &frost.Configuration{ + Ciphersuite: test.Ciphersuite, + Threshold: test.threshold, + MaxSigners: test.maxSigners, + GroupPublicKey: test.ECGroup().Base(), + SignerPublicKeyShares: publicKeyShares, + } + + if err := configuration.Init(); err == nil || !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } + }) +} + +func TestConfiguration_VerifySignerPublicKeys_InvalidNumber(t *testing.T) { + expectedErrorPrefix := "invalid number of public keys (lower than threshold or above maximum)" + + ciphersuite := frost.Ristretto255 + threshold := uint64(2) + maxSigners := uint64(3) + + keyShares, groupPublicKey, _ := debug.TrustedDealerKeygen(ciphersuite, nil, threshold, maxSigners) + publicKeyShares := getPublicKeyShares(keyShares) + + // nil + configuration := &frost.Configuration{ + Ciphersuite: ciphersuite, + Threshold: threshold, + MaxSigners: maxSigners, + GroupPublicKey: groupPublicKey, + SignerPublicKeyShares: nil, + } + + if err := configuration.Init(); err == nil || !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } + + // empty + configuration.SignerPublicKeyShares = []*keys.PublicKeyShare{} + + if err := configuration.Init(); err == nil || !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } + + // too few + configuration.SignerPublicKeyShares = publicKeyShares[:threshold-1] + + if err := configuration.Init(); err == nil || !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } + + // too many + configuration.SignerPublicKeyShares = append(publicKeyShares, &keys.PublicKeyShare{}) + + if err := configuration.Init(); err == nil || !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } +} + +func TestConfiguration_VerifySignerPublicKeys_Nil(t *testing.T) { + expectedErrorPrefix := "empty public key share at index 1" + + ciphersuite := frost.Ristretto255 + threshold := uint64(2) + maxSigners := uint64(3) + + keyShares, groupPublicKey, _ := debug.TrustedDealerKeygen(ciphersuite, nil, threshold, maxSigners) + publicKeyShares := getPublicKeyShares(keyShares) + publicKeyShares[threshold-1] = nil + + configuration := &frost.Configuration{ + Ciphersuite: ciphersuite, + Threshold: threshold, + MaxSigners: maxSigners, + GroupPublicKey: groupPublicKey, + SignerPublicKeyShares: publicKeyShares, + } + + if err := configuration.Init(); err == nil || !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } +} + +func TestConfiguration_VerifySignerPublicKeys_BadPublicKey(t *testing.T) { + ciphersuite := frost.Ristretto255 + threshold := uint64(2) + maxSigners := uint64(3) + + keyShares, groupPublicKey, _ := debug.TrustedDealerKeygen(ciphersuite, nil, threshold, maxSigners) + publicKeyShares := getPublicKeyShares(keyShares) + + configuration := &frost.Configuration{ + Ciphersuite: ciphersuite, + Threshold: threshold, + MaxSigners: maxSigners, + GroupPublicKey: groupPublicKey, + SignerPublicKeyShares: publicKeyShares, + } + + // nil pk + expectedErrorPrefix := fmt.Sprintf( + "invalid public key for participant %d, the key is nil", + configuration.SignerPublicKeyShares[threshold-1].ID, + ) + configuration.SignerPublicKeyShares[threshold-1].PublicKey = nil + + if err := configuration.Init(); err == nil || !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } + + // identity + expectedErrorPrefix = fmt.Sprintf( + "invalid public key for participant %d, the key is the identity element", + configuration.SignerPublicKeyShares[threshold-1].ID, + ) + configuration.SignerPublicKeyShares[threshold-1].PublicKey = ciphersuite.ECGroup().NewElement() + + if err := configuration.Init(); err == nil || !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } + + // generator + expectedErrorPrefix = fmt.Sprintf( + "invalid public key for participant %d, the key is the group generator (base element)", + configuration.SignerPublicKeyShares[threshold-1].ID, + ) + configuration.SignerPublicKeyShares[threshold-1].PublicKey = ciphersuite.ECGroup().Base() + + if err := configuration.Init(); err == nil || !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } +} + +func TestConfiguration_VerifySignerPublicKeys_Duplicate_Identifiers(t *testing.T) { + expectedErrorPrefix := "found duplicate identifier for signer 1" + + ciphersuite := frost.Ristretto255 + threshold := uint64(2) + maxSigners := uint64(3) + + keyShares, groupPublicKey, _ := debug.TrustedDealerKeygen(ciphersuite, nil, threshold, maxSigners) + publicKeyShares := getPublicKeyShares(keyShares) + + configuration := &frost.Configuration{ + Ciphersuite: ciphersuite, + Threshold: threshold, + MaxSigners: maxSigners, + GroupPublicKey: groupPublicKey, + SignerPublicKeyShares: publicKeyShares, + } + + // duplicate id + id1 := configuration.SignerPublicKeyShares[0].ID + configuration.SignerPublicKeyShares[1].ID = id1 + + if err := configuration.Init(); err == nil || !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } +} + +func TestConfiguration_VerifySignerPublicKeys_Duplicate_PublicKeys(t *testing.T) { + expectedErrorPrefix := "found duplicate public keys for signers 2 and 1" + + ciphersuite := frost.Ristretto255 + threshold := uint64(2) + maxSigners := uint64(3) + + keyShares, groupPublicKey, _ := debug.TrustedDealerKeygen(ciphersuite, nil, threshold, maxSigners) + publicKeyShares := getPublicKeyShares(keyShares) + + configuration := &frost.Configuration{ + Ciphersuite: ciphersuite, + Threshold: threshold, + MaxSigners: maxSigners, + GroupPublicKey: groupPublicKey, + SignerPublicKeyShares: publicKeyShares, + } + + // duplicate id + pk1 := configuration.SignerPublicKeyShares[0].PublicKey.Copy() + configuration.SignerPublicKeyShares[1].PublicKey = pk1 + + if err := configuration.Init(); err == nil || !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } +} + +func TestConfiguration_ValidatePublicKeyShare_InvalidConfiguration(t *testing.T) { + expectedErrorPrefix := "invalid group public key, the key is nil" + tt := &tableTest{ + Ciphersuite: frost.Ristretto255, + threshold: 3, + maxSigners: 5, + } + configuration := &frost.Configuration{ + Ciphersuite: tt.Ciphersuite, + Threshold: tt.threshold, + MaxSigners: tt.maxSigners, + GroupPublicKey: nil, + SignerPublicKeyShares: nil, + } + + if err := configuration.ValidatePublicKeyShare(nil); err == nil || + !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } +} + +func TestConfiguration_ValidatePublicKeyShare_Nil(t *testing.T) { + expectedErrorPrefix := "public key share is nil" + tt := &tableTest{ + Ciphersuite: frost.Ristretto255, + threshold: 2, + maxSigners: 3, + } + configuration, _ := makeConfAndShares(t, tt) + + if err := configuration.ValidatePublicKeyShare(nil); err == nil || err.Error() != expectedErrorPrefix { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } +} + +func TestConfiguration_ValidatePublicKeyShare_WrongGroup(t *testing.T) { + expectedErrorPrefix := "key share has invalid group parameter, want ristretto255_XMD:SHA-512_R255MAP_RO_ got 0" + tt := &tableTest{ + Ciphersuite: frost.Ristretto255, + threshold: 2, + maxSigners: 3, + } + configuration, _ := makeConfAndShares(t, tt) + + pks := &keys.PublicKeyShare{ + Group: 0, + } + + if err := configuration.ValidatePublicKeyShare(pks); err == nil || err.Error() != expectedErrorPrefix { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } +} + +func TestConfiguration_ValidatePublicKeyShare_ID0(t *testing.T) { + expectedErrorPrefix := "invalid identifier for public key share, the identifier is 0" + tt := &tableTest{ + Ciphersuite: frost.Ristretto255, + threshold: 2, + maxSigners: 3, + } + configuration, _ := makeConfAndShares(t, tt) + + pks := &keys.PublicKeyShare{ + Group: tt.ECGroup(), + ID: 0, + } + + if err := configuration.ValidatePublicKeyShare(pks); err == nil || err.Error() != expectedErrorPrefix { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } +} + +func TestConfiguration_ValidatePublicKeyShare_InvalidID(t *testing.T) { + expectedErrorPrefix := "invalid identifier for public key share, the identifier 4 is above authorized range [1:3]" + tt := &tableTest{ + Ciphersuite: frost.Ristretto255, + threshold: 2, + maxSigners: 3, + } + configuration, _ := makeConfAndShares(t, tt) + + pks := &keys.PublicKeyShare{ + Group: tt.ECGroup(), + ID: tt.maxSigners + 1, + } + + if err := configuration.ValidatePublicKeyShare(pks); err == nil || err.Error() != expectedErrorPrefix { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } +} + +func TestConfiguration_ValidatePublicKeyShare_InvalidPublicKey(t *testing.T) { + expectedErrorPrefix := "invalid public key for participant 1, the key is the group generator (base element)" + tt := &tableTest{ + Ciphersuite: frost.Ristretto255, + threshold: 2, + maxSigners: 3, + } + configuration, _ := makeConfAndShares(t, tt) + + pks := &keys.PublicKeyShare{ + Group: tt.ECGroup(), + ID: 1, + PublicKey: tt.ECGroup().Base(), + } + + if err := configuration.ValidatePublicKeyShare(pks); err == nil || err.Error() != expectedErrorPrefix { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } +} + +func TestConfiguration_ValidateKeyShare_InvalidConfiguration(t *testing.T) { + expectedErrorPrefix := "invalid group public key, the key is nil" + tt := &tableTest{ + Ciphersuite: frost.Ristretto255, + threshold: 3, + maxSigners: 5, + } + configuration := &frost.Configuration{ + Ciphersuite: tt.Ciphersuite, + Threshold: tt.threshold, + MaxSigners: tt.maxSigners, + GroupPublicKey: nil, + SignerPublicKeyShares: nil, + } + + if err := configuration.ValidateKeyShare(nil); err == nil || err.Error() != expectedErrorPrefix { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } +} + +func TestConfiguration_ValidateKeyShare_Nil(t *testing.T) { + expectedErrorPrefix := "provided key share is nil" + tt := &tableTest{ + Ciphersuite: frost.Ristretto255, + threshold: 2, + maxSigners: 3, + } + configuration, _ := makeConfAndShares(t, tt) + + if err := configuration.ValidateKeyShare(nil); err == nil || err.Error() != expectedErrorPrefix { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } +} + +func TestConfiguration_ValidateKeyShare_InvalidGroupPublicKey(t *testing.T) { + expectedErrorPrefix := "the key share's group public key does not match the one in the configuration" + tt := &tableTest{ + Ciphersuite: frost.Ristretto255, + threshold: 2, + maxSigners: 3, + } + configuration, keyShares := makeConfAndShares(t, tt) + keyShare := keyShares[0] + + keyShare.GroupPublicKey = nil + if err := configuration.ValidateKeyShare(keyShare); err == nil || err.Error() != expectedErrorPrefix { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } + + keyShare.GroupPublicKey = tt.ECGroup().NewElement() + if err := configuration.ValidateKeyShare(keyShare); err == nil || err.Error() != expectedErrorPrefix { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } + + keyShare.GroupPublicKey.Base() + if err := configuration.ValidateKeyShare(keyShare); err == nil || err.Error() != expectedErrorPrefix { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } +} + +func TestConfiguration_ValidateKeyShare_BadPublicKeyShare(t *testing.T) { + expectedErrorPrefix := "invalid public key for participant 1, the key is nil" + tt := &tableTest{ + Ciphersuite: frost.Ristretto255, + threshold: 2, + maxSigners: 3, + } + configuration, keyShares := makeConfAndShares(t, tt) + keyShare := keyShares[0] + + keyShare.PublicKey = nil + if err := configuration.ValidateKeyShare(keyShare); err == nil || err.Error() != expectedErrorPrefix { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } +} + +func TestConfiguration_ValidateKeyShare_InvalidSecretKey(t *testing.T) { + expectedErrorPrefix := "provided key share has invalid secret key" + tt := &tableTest{ + Ciphersuite: frost.Ristretto255, + threshold: 2, + maxSigners: 3, + } + configuration, keyShares := makeConfAndShares(t, tt) + keyShare := keyShares[0] + + keyShare.Secret = nil + if err := configuration.ValidateKeyShare(keyShare); err == nil || err.Error() != expectedErrorPrefix { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } + + keyShare.Secret = tt.ECGroup().NewScalar() + if err := configuration.ValidateKeyShare(keyShare); err == nil || err.Error() != expectedErrorPrefix { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } +} + +func TestConfiguration_ValidateKeyShare_KeysNotMatching(t *testing.T) { + expectedErrorPrefix := "provided key share has non-matching secret and public keys" + tt := &tableTest{ + Ciphersuite: frost.Ristretto255, + threshold: 2, + maxSigners: 3, + } + configuration, keyShares := makeConfAndShares(t, tt) + keyShare := keyShares[0] + + random := tt.ECGroup().NewScalar().Random() + keyShare.PublicKey = tt.ECGroup().Base().Multiply(random) + if err := configuration.ValidateKeyShare(keyShare); err == nil || err.Error() != expectedErrorPrefix { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } +} + +func TestConfiguration_ValidateKeyShare_SignerIDNotRegistered(t *testing.T) { + expectedErrorPrefix := "provided key share has no registered signer identifier in the configuration" + tt := &tableTest{ + Ciphersuite: frost.Ristretto255, + threshold: 2, + maxSigners: 3, + } + configuration, keyShares := makeConfAndShares(t, tt) + + pks := make([]*keys.PublicKeyShare, len(keyShares)-1) + for i, ks := range keyShares[1:] { + pks[i] = ks.Public() + } + + configuration.SignerPublicKeyShares = pks + + if err := configuration.ValidateKeyShare(keyShares[0]); err == nil || err.Error() != expectedErrorPrefix { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } +} + +func TestConfiguration_ValidateKeyShare_WrongPublicKey(t *testing.T) { + expectedErrorPrefix := "provided key share has a different public key than the one registered for that signer in the configuration" + tt := &tableTest{ + Ciphersuite: frost.Ristretto255, + threshold: 2, + maxSigners: 3, + } + configuration, keyShares := makeConfAndShares(t, tt) + + random := tt.ECGroup().NewScalar().Random() + keyShare := &keys.KeyShare{ + Secret: random, + GroupPublicKey: keyShares[0].GroupPublicKey, + PublicKeyShare: secretsharing.PublicKeyShare{ + PublicKey: tt.ECGroup().Base().Multiply(random), + ID: keyShares[0].ID, + Group: keyShares[0].Group, + }, + } + + if err := configuration.ValidateKeyShare(keyShare); err == nil || err.Error() != expectedErrorPrefix { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } +} + +func TestConfiguration_Signer_NotVerified(t *testing.T) { + ciphersuite := frost.Ristretto255 + threshold := uint64(2) + maxSigners := uint64(3) + + keyShares, groupPublicKey, _ := debug.TrustedDealerKeygen(ciphersuite, nil, threshold, maxSigners) + publicKeyShares := getPublicKeyShares(keyShares) + + configuration := &frost.Configuration{ + Ciphersuite: ciphersuite, + Threshold: threshold, + MaxSigners: maxSigners, + GroupPublicKey: groupPublicKey, + SignerPublicKeyShares: publicKeyShares, + } + + if _, err := configuration.Signer(keyShares[0]); err != nil { + t.Fatal(err) + } +} + +func TestConfiguration_Signer_BadConfig(t *testing.T) { + expectedErrorPrefix := internal.ErrInvalidCiphersuite + ciphersuite := frost.Ristretto255 + threshold := uint64(2) + maxSigners := uint64(3) + + keyShares, groupPublicKey, _ := debug.TrustedDealerKeygen(ciphersuite, nil, threshold, maxSigners) + publicKeyShares := getPublicKeyShares(keyShares) + + configuration := &frost.Configuration{ + Ciphersuite: 2, + Threshold: threshold, + MaxSigners: maxSigners, + GroupPublicKey: groupPublicKey, + SignerPublicKeyShares: publicKeyShares, + } + + if _, err := configuration.Signer(keyShares[0]); err == nil || + !strings.HasPrefix(err.Error(), expectedErrorPrefix.Error()) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } +} + +func TestConfiguration_Singer_BadKeyShare(t *testing.T) { + expectedErrorPrefix := "provided key share is nil" + tt := &tableTest{ + Ciphersuite: frost.Ristretto255, + threshold: 2, + maxSigners: 3, + } + + configuration := makeConf(t, tt) + + if _, err := configuration.Signer(nil); err == nil || + !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } +} + +func TestConfiguration_VerifySignatureShare_BadPrep(t *testing.T) { + expectedErrorPrefix := internal.ErrInvalidCiphersuite + + ciphersuite := frost.Ristretto255 + threshold := uint64(2) + maxSigners := uint64(3) + + keyShares, groupPublicKey, _ := debug.TrustedDealerKeygen(ciphersuite, nil, threshold, maxSigners) + publicKeyShares := getPublicKeyShares(keyShares) + + configuration := &frost.Configuration{ + Ciphersuite: 2, + Threshold: threshold, + MaxSigners: maxSigners, + GroupPublicKey: groupPublicKey, + SignerPublicKeyShares: publicKeyShares, + } + + if err := configuration.VerifySignatureShare(nil, nil, nil); err == nil || + !strings.HasPrefix(err.Error(), expectedErrorPrefix.Error()) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } +} + +func TestConfiguration_VerifySignatureShare_NilShare(t *testing.T) { + expectedErrorPrefix := "nil signature share" + tt := &tableTest{ + Ciphersuite: frost.Ristretto255, + threshold: 2, + maxSigners: 3, + } + message := []byte("message") + configuration, signers := fullSetup(t, tt) + coms := make(frost.CommitmentList, len(signers)) + + for i, s := range signers { + coms[i] = s.Commit() + } + + if err := configuration.VerifySignatureShare(nil, message, coms); err == nil || + !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } +} + +func TestConfiguration_VerifySignatureShare_SignerID0(t *testing.T) { + expectedErrorPrefix := "invalid identifier for signer in signature share, the identifier is 0" + tt := &tableTest{ + Ciphersuite: frost.Ristretto255, + threshold: 2, + maxSigners: 3, + } + message := []byte("message") + configuration, signers := fullSetup(t, tt) + coms := make(frost.CommitmentList, len(signers)) + + for i, s := range signers { + coms[i] = s.Commit() + } + + sigShare, err := signers[0].Sign(message, coms) + if err != nil { + t.Fatal(err) + } + + sigShare.SignerIdentifier = 0 + + if err := configuration.VerifySignatureShare(sigShare, message, coms); err == nil || + !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } +} + +func TestConfiguration_VerifySignatureShare_InvalidSignerID(t *testing.T) { + tt := &tableTest{ + Ciphersuite: frost.Ristretto255, + threshold: 2, + maxSigners: 3, + } + message := []byte("message") + configuration, signers := fullSetup(t, tt) + coms := make(frost.CommitmentList, len(signers)) + + for i, s := range signers { + coms[i] = s.Commit() + } + + sigShare, err := signers[0].Sign(message, coms) + if err != nil { + t.Fatal(err) + } + + sigShare.SignerIdentifier = tt.maxSigners + 1 + + expectedErrorPrefix := fmt.Sprintf( + "invalid identifier for signer in signature share, the identifier %d is above authorized range [1:%d]", + sigShare.SignerIdentifier, + tt.maxSigners, + ) + + if err := configuration.VerifySignatureShare(sigShare, message, coms); err == nil || + !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } +} + +func TestConfiguration_VerifySignatureShare_BadGroup(t *testing.T) { + expectedErrorPrefix := "signature share has invalid group parameter, want ristretto255_XMD:SHA-512_R255MAP_RO_ got 2" + tt := &tableTest{ + Ciphersuite: frost.Ristretto255, + threshold: 2, + maxSigners: 3, + } + message := []byte("message") + configuration, signers := fullSetup(t, tt) + coms := make(frost.CommitmentList, len(signers)) + + for i, s := range signers { + coms[i] = s.Commit() + } + + sigShare, err := signers[0].Sign(message, coms) + if err != nil { + t.Fatal(err) + } + + sigShare.Group = 2 + + if err := configuration.VerifySignatureShare(sigShare, message, coms); err == nil || + !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } +} + +func TestConfiguration_VerifySignatureShare_MissingPublicKey(t *testing.T) { + tt := &tableTest{ + Ciphersuite: frost.Ristretto255, + threshold: 2, + maxSigners: 3, + } + message := []byte("message") + configuration, signers := fullSetup(t, tt) + coms := make(frost.CommitmentList, len(signers)) + + for i, s := range signers { + coms[i] = s.Commit() + } + + sigShare, err := signers[0].Sign(message, coms) + if err != nil { + t.Fatal(err) + } + + configuration.SignerPublicKeyShares = slices.Delete(configuration.SignerPublicKeyShares, 0, 1) + expectedErrorPrefix := fmt.Sprintf("no public key registered for signer 1") + + if err := configuration.VerifySignatureShare(sigShare, message, coms[1:]); err == nil || + !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } +} + +func TestConfiguration_VerifySignatureShare_MissingCommitment(t *testing.T) { + expectedErrorPrefix := "commitment for signer 1 is missing" + tt := &tableTest{ + Ciphersuite: frost.Ristretto255, + threshold: 2, + maxSigners: 4, + } + message := []byte("message") + configuration, signers := fullSetup(t, tt) + coms := make(frost.CommitmentList, len(signers)) + + for i, s := range signers { + coms[i] = s.Commit() + } + + sigShare, err := signers[0].Sign(message, coms) + if err != nil { + t.Fatal(err) + } + + if err := configuration.VerifySignatureShare(sigShare, message, coms[1:]); err == nil || + !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } +} + +func TestConfiguration_VerifySignatureShare_BadCommitment_BadSignerID(t *testing.T) { + tt := &tableTest{ + Ciphersuite: frost.Ristretto255, + threshold: 2, + maxSigners: 3, + } + message := []byte("message") + configuration, signers := fullSetup(t, tt) + coms := make(frost.CommitmentList, len(signers)) + + for i, s := range signers { + coms[i] = s.Commit() + } + + sigShare, err := signers[0].Sign(message, coms) + if err != nil { + t.Fatal(err) + } + + coms[1].SignerID = 0 + expectedErrorPrefix := fmt.Sprintf( + "invalid list of commitments: invalid identifier for signer in commitment %d, the identifier is 0", + coms[1].CommitmentID, + ) + + if err := configuration.VerifySignatureShare(sigShare, message, coms); err == nil || + !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } +} + +func TestConfiguration_VerifySignatureShare_InvalidSignatureShare(t *testing.T) { + expectedErrorPrefix := "invalid signature share for signer 1" + tt := &tableTest{ + Ciphersuite: frost.Ristretto255, + threshold: 2, + maxSigners: 3, + } + message := []byte("message") + configuration, signers := fullSetup(t, tt) + coms := make(frost.CommitmentList, len(signers)) + + for i, s := range signers { + coms[i] = s.Commit() + } + + sigShare := &frost.SignatureShare{ + SignatureShare: tt.Ciphersuite.ECGroup().NewScalar().Random(), + SignerIdentifier: 1, + Group: tt.Ciphersuite.ECGroup(), + } + + if err := configuration.VerifySignatureShare(sigShare, message, coms); err == nil || + !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } +} + +func TestConfiguration_AggregateSignatures_InvalidConfiguration(t *testing.T) { + expectedErrorPrefix := "invalid group public key, the key is nil" + tt := &tableTest{ + Ciphersuite: frost.Ristretto255, + threshold: 3, + maxSigners: 5, + } + configuration := &frost.Configuration{ + Ciphersuite: tt.Ciphersuite, + Threshold: tt.threshold, + MaxSigners: tt.maxSigners, + GroupPublicKey: nil, + SignerPublicKeyShares: nil, + } + + if _, err := configuration.AggregateSignatures(nil, nil, nil, false); err == nil || + !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } +} + +func TestConfiguration_AggregateSignatures_InvalidCommitments(t *testing.T) { + expectedErrorPrefix := "invalid list of commitments: too few commitments: expected at least 3 but got 2" + tt := &tableTest{ + Ciphersuite: frost.Ristretto255, + threshold: 3, + maxSigners: 5, + } + configuration, signers := fullSetup(t, tt) + coms := make(frost.CommitmentList, len(signers)) + + for i, s := range signers { + coms[i] = s.Commit() + } + + if _, err := configuration.AggregateSignatures(nil, nil, coms[:2], false); err == nil || + !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } +} + +func TestConfiguration_AggregateSignatures_BadSigShare1(t *testing.T) { + expectedErrorPrefix := "invalid signature share for signer 2" + tt := &tableTest{ + Ciphersuite: frost.Ristretto255, + threshold: 2, + maxSigners: 3, + } + message := []byte("message") + configuration, signers := fullSetup(t, tt) + + coms := make(frost.CommitmentList, len(signers)) + for i, s := range signers { + coms[i] = s.Commit() + } + + sigShares := make([]*frost.SignatureShare, len(signers)) + for i, s := range signers { + var err error + sigShares[i], err = s.Sign(message, coms) + if err != nil { + t.Fatal(err) + } + } + + sigShares[1].SignatureShare.Random() + + if _, err := configuration.AggregateSignatures(message, sigShares, coms, true); err == nil || + !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } +} + +func TestConfiguration_AggregateSignatures_BadSigShare2(t *testing.T) { + expectedErrorPrefix := "nil signature share" + tt := &tableTest{ + Ciphersuite: frost.Ristretto255, + threshold: 2, + maxSigners: 3, + } + message := []byte("message") + configuration, signers := fullSetup(t, tt) + + coms := make(frost.CommitmentList, len(signers)) + for i, s := range signers { + coms[i] = s.Commit() + } + + sigShares := make([]*frost.SignatureShare, len(signers)) + for i, s := range signers { + var err error + sigShares[i], err = s.Sign(message, coms) + if err != nil { + t.Fatal(err) + } + } + + sigShares[1] = nil + + if _, err := configuration.AggregateSignatures(message, sigShares, coms, false); err == nil || + !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } +} + +func TestConfiguration_AggregateSignatures_BadSigShare3(t *testing.T) { + expectedErrorPrefix := "invalid signature share (nil or zero scalar)" + tt := &tableTest{ + Ciphersuite: frost.Ristretto255, + threshold: 2, + maxSigners: 3, + } + message := []byte("message") + configuration, signers := fullSetup(t, tt) + + coms := make(frost.CommitmentList, len(signers)) + for i, s := range signers { + coms[i] = s.Commit() + } + + sigShares := make([]*frost.SignatureShare, len(signers)) + for i, s := range signers { + var err error + sigShares[i], err = s.Sign(message, coms) + if err != nil { + t.Fatal(err) + } + } + + sigShares[1].SignatureShare.Zero() + + if _, err := configuration.AggregateSignatures(message, sigShares, coms, false); err == nil || + !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } +} diff --git a/tests/dkg_test.go b/tests/dkg_test.go index 2a97fb9..78cc701 100644 --- a/tests/dkg_test.go +++ b/tests/dkg_test.go @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT // -// Copyright (C) 2023 Daniel Bourdrez. All Rights Reserved. +// Copyright (C) 2024 Daniel Bourdrez. All Rights Reserved. // // This source code is licensed under the MIT license found in the // LICENSE file in the root directory of this source tree or at @@ -12,219 +12,93 @@ import ( "testing" group "github.com/bytemare/crypto" - secretsharing "github.com/bytemare/secret-sharing" + "github.com/bytemare/dkg" - "github.com/bytemare/frost" - "github.com/bytemare/frost/dkg" - "github.com/bytemare/frost/internal" + "github.com/bytemare/frost/keys" ) -// testUnit holds a participant and its return and input values during the protocol. -type testUnit struct { - participant *dkg.Participant - r1Data *dkg.Round1Data - secret *group.Scalar - verificationShare *group.Element - publicKey *group.Element - r2OutputData []*dkg.Round2Data - r2InputData []*dkg.Round2Data -} - -// TestDKG verifies -// - execution of the protocol with any number of participants and threshold, and no errors. -// - the correctness of each verification share. -// - the correctness of the group public key. -// - the correctness of the secret key recovery with regard to the public key. -func TestDKG(t *testing.T) { - conf := frost.Ristretto255.Configuration() - g := conf.Ciphersuite.Group - maxSigners := 5 - quals := []int{1, 3, 5} // = threshold - - var err error - - // Vector of participant units. - units := make([]*testUnit, maxSigners) - for i := 0; i < maxSigners; i++ { - id := internal.IntegerToScalar(conf.Ciphersuite.Group, i+1) - units[i] = &testUnit{ - participant: dkg.NewParticipant(conf.Ciphersuite, id, maxSigners, len(quals)), - r2InputData: make([]*dkg.Round2Data, 0, maxSigners-1), - } - } - - // Step 1: Init. - for _, unit := range units { - unit.r1Data = unit.participant.Init() - } - - // Step 2: assemble packages. - r1Data := make([]*dkg.Round1Data, maxSigners) - for i, unit := range units { - r1Data[i] = unit.r1Data - } - - // Step 3: Continue. - for _, unit := range units { - unit.r2OutputData, err = unit.participant.Continue(r1Data) +func dkgMakeParticipants(t *testing.T, ciphersuite dkg.Ciphersuite, maxSigners, threshold uint64) []*dkg.Participant { + ps := make([]*dkg.Participant, 0, maxSigners) + for i := range maxSigners { + p, err := ciphersuite.NewParticipant(i+1, uint(maxSigners), uint(threshold)) if err != nil { t.Fatal(err) } - } - // Step 4: assemble packages. - for i, uniti := range units { - for j, unitj := range units { - if i == j { - continue - } - - for _, p := range unitj.r2OutputData { - if p.ReceiverIdentifier.Equal(uniti.participant.Identifier) == 1 { - uniti.r2InputData = append(uniti.r2InputData, p) - break - } - } - } + ps = append(ps, p) } - // Step 5: Finalize. - for _, unit := range units { - unit.secret, unit.verificationShare, unit.publicKey, err = unit.participant.Finalize( - r1Data, - unit.r2InputData, - ) - if err != nil { - t.Fatal(err) - } - } + return ps +} - // Verify individual verification shares. - for _, unit := range units { - verifPk := dkg.ComputeVerificationShare(g, unit.participant.Identifier, r1Data) - if verifPk.Equal(unit.verificationShare) != 1 { - t.Fatal("invalid verification key") - } - } +func runDKG( + t *testing.T, + g group.Group, + threshold, maxSigners uint64, +) ([]*keys.KeyShare, *group.Element, []*group.Element) { + c := dkg.Ciphersuite(g) - // Compare group public keys. - p1g := units[0].publicKey - for _, unit := range units[1:] { - if p1g.Equal(unit.publicKey) != 1 { - t.Fatal("expected equality") - } - } + // valid r1DataSet set with and without own package + participants := dkgMakeParticipants(t, c, maxSigners, threshold) + r1 := make([]*dkg.Round1Data, maxSigners) + commitments := make([][]*group.Element, maxSigners) - // Verify the individual secret shares by combining a subset of them. - keyShares := make([]*secretsharing.KeyShare, len(quals)) - for i, ii := range quals { - id := internal.IntegerToScalar(conf.Ciphersuite.Group, ii) - - for _, unit := range units { - if id.Equal(unit.participant.Identifier) == 1 { - keyShares[i] = &secretsharing.KeyShare{ - Identifier: unit.participant.Identifier, - SecretKey: unit.secret, - } - } - } + // Step 1: Start and assemble packages. + for i := range maxSigners { + r1[i] = participants[i].Start() + commitments[i] = r1[i].Commitment } - secret, err := secretsharing.Combine(g, uint(len(quals)), keyShares) + pubKey, err := dkg.GroupPublicKeyFromRound1(c, r1) if err != nil { t.Fatal(err) } - pk := g.Base().Multiply(secret) - if pk.Equal(p1g) != 1 { - t.Fatal("expected recovered secret to be compatible with public key") - } -} - -// TestDKG_InvalidPOK verifies whether an invalid signature is detected and an error is returned. -func TestDKG_InvalidPOK(t *testing.T) { - conf := frost.Ristretto255.Configuration() - g := conf.Ciphersuite.Group - - maxSigners := 2 - threshold := 1 - - one := g.NewScalar().One() - p1 := dkg.NewParticipant(conf.Ciphersuite, one, maxSigners, threshold) - - two := g.NewScalar().One().Add(g.NewScalar().One()) - p2 := dkg.NewParticipant(conf.Ciphersuite, two, maxSigners, threshold) - - r1P1 := p1.Init() - r1P2 := p2.Init() - - r1P2.ProofOfKnowledge.Z = g.NewScalar().Random() - - r1Data := []*dkg.Round1Data{r1P1, r1P2} - - if _, err := p1.Continue(r1Data); err == nil { - t.Fatal("expected error on invalid signature") - } -} + // Step 2: Continue and assemble + triage packages. + r2 := make(map[uint64][]*dkg.Round2Data, maxSigners) + for i := range maxSigners { + r, err := participants[i].Continue(r1) + if err != nil { + t.Fatal(err) + } -// SimulateDKG generates sharded keys for maxSigners participant without a trusted dealer, and returns these shares -// and the group's public key. This function is used in tests and examples. -func SimulateDKG( - conf *frost.Configuration, - maxSigners, threshold int, -) ([]*secretsharing.KeyShare, []*group.Element, *group.Element) { - g := conf.Ciphersuite.Group - - // Create participants. - participants := make([]*dkg.Participant, maxSigners) - for i := 0; i < maxSigners; i++ { - id := internal.IntegerToScalar(conf.Ciphersuite.Group, i+1) - participants[i] = dkg.NewParticipant(conf.Ciphersuite, id, maxSigners, threshold) + for id, data := range r { + if r2[id] == nil { + r2[id] = make([]*dkg.Round2Data, 0, maxSigners-1) + } + r2[id] = append(r2[id], data) + } } - // Step 1 & 2. - r1Data := make([]*dkg.Round1Data, maxSigners) - for i, p := range participants { - r1Data[i] = p.Init() + // Step 3: Clean the proofs. + // This must be called by each participant on their copy of the r1DataSet. + for _, d := range r1 { + d.ProofOfKnowledge.Clear() } - // Step 3 & 4. - r2Data := make(map[string][]*dkg.Round2Data) - for _, p := range participants { - id := string(p.Identifier.Encode()) - r2Data[id] = make([]*dkg.Round2Data, 0, maxSigners-1) - } + // Step 4: Finalize and test outputs. + keyShares := make([]*keys.KeyShare, 0, maxSigners) for _, p := range participants { - r2DataI, err := p.Continue(r1Data) + keyShare, err := p.Finalize(r1, r2[p.Identifier]) if err != nil { - panic(err) + t.Fatal() } - for _, r2d := range r2DataI { - id := string(r2d.ReceiverIdentifier.Encode()) - r2Data[id] = append(r2Data[id], r2d) + if keyShare.GroupPublicKey.Equal(pubKey) != 1 { + t.Fatalf("expected same public key") } - } - // Step 5. - secretShares := make([]*secretsharing.KeyShare, maxSigners) - publicShares := make([]*group.Element, maxSigners) - groupPublicKey := g.NewElement() - for i, p := range participants { - id := string(p.Identifier.Encode()) - secret, public, pk, err := p.Finalize(r1Data, r2Data[id]) - if err != nil { - panic(err) + if keyShare.PublicKey.Equal(g.Base().Multiply(keyShare.SecretKey())) != 1 { + t.Fatal("expected equality") } - secretShares[i] = &secretsharing.KeyShare{ - Identifier: p.Identifier, - SecretKey: secret, + if err := dkg.VerifyPublicKey(c, p.Identifier, keyShare.PublicKey, commitments); err != nil { + t.Fatal(err) } - publicShares[i] = public - groupPublicKey = pk + + keyShares = append(keyShares, (*keys.KeyShare)(keyShare)) } - return secretShares, publicShares, groupPublicKey + return keyShares, pubKey, nil } diff --git a/tests/encoding_test.go b/tests/encoding_test.go new file mode 100644 index 0000000..3de573e --- /dev/null +++ b/tests/encoding_test.go @@ -0,0 +1,1216 @@ +// SPDX-License-Identifier: MIT +// +// Copyright (C) 2024 Daniel Bourdrez. All Rights Reserved. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree or at +// https://spdx.org/licenses/MIT.html + +package frost_test + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "slices" + "strings" + "testing" + + group "github.com/bytemare/crypto" + + "github.com/bytemare/frost" + "github.com/bytemare/frost/debug" + "github.com/bytemare/frost/internal" + "github.com/bytemare/frost/keys" +) + +func makeConfAndShares(t *testing.T, test *tableTest) (*frost.Configuration, []*keys.KeyShare) { + keyShares, groupPublicKey, _ := debug.TrustedDealerKeygen(test.Ciphersuite, nil, test.threshold, test.maxSigners) + publicKeyShares := getPublicKeyShares(keyShares) + + configuration := &frost.Configuration{ + Ciphersuite: test.Ciphersuite, + Threshold: test.threshold, + MaxSigners: test.maxSigners, + GroupPublicKey: groupPublicKey, + SignerPublicKeyShares: publicKeyShares, + } + + if err := configuration.Init(); err != nil { + t.Fatal(err) + } + + return configuration, keyShares +} + +func makeConf(t *testing.T, test *tableTest) *frost.Configuration { + c, _ := makeConfAndShares(t, test) + return c +} + +func getPublicKeyShares(keyShares []*keys.KeyShare) []*keys.PublicKeyShare { + publicKeyShares := make([]*keys.PublicKeyShare, 0, len(keyShares)) + for _, ks := range keyShares { + publicKeyShares = append(publicKeyShares, ks.Public()) + } + + return publicKeyShares +} + +func fullSetup(t *testing.T, test *tableTest) (*frost.Configuration, []*frost.Signer) { + configuration, keyShares := makeConfAndShares(t, test) + signers := make([]*frost.Signer, test.maxSigners) + + for i, keyShare := range keyShares { + s, err := configuration.Signer(keyShare) + if err != nil { + t.Fatal(err) + } + + signers[i] = s + } + + return configuration, signers +} + +func makeSigners(t *testing.T, test *tableTest) []*frost.Signer { + _, s := fullSetup(t, test) + return s +} + +func compareConfigurations(t *testing.T, c1, c2 *frost.Configuration, expectedMatch bool) { + if c1 == nil || c2 == nil { + t.Fatal("nil config") + } + + if c1.Ciphersuite != c2.Ciphersuite && expectedMatch { + t.Fatalf("expected matching ciphersuite: %q / %q", c1.Ciphersuite, c2.Ciphersuite) + } + + if c1.Threshold != c2.Threshold && expectedMatch { + t.Fatalf("expected matching threshold: %q / %q", c1.Threshold, c2.Threshold) + } + + if c1.MaxSigners != c2.MaxSigners && expectedMatch { + t.Fatalf("expected matching max signers: %q / %q", c1.MaxSigners, c2.MaxSigners) + } + + if ((c1.GroupPublicKey == nil || c2.GroupPublicKey == nil) || (c1.GroupPublicKey.Equal(c2.GroupPublicKey) != 1)) && + expectedMatch { + t.Fatalf("expected matching GroupPublicKey: %q / %q", c1.Ciphersuite, c2.Ciphersuite) + } + + if len(c1.SignerPublicKeyShares) != len(c2.SignerPublicKeyShares) { + t.Fatalf( + "expected matching SignerPublicKeyShares lengths: %q / %q", + len(c1.SignerPublicKeyShares), + len(c2.SignerPublicKeyShares), + ) + } + + for i, p1 := range c1.SignerPublicKeyShares { + p2 := c2.SignerPublicKeyShares[i] + if err := comparePublicKeyShare(p1, p2); !expectedMatch && err != nil { + t.Fatal(err) + } + } +} + +func comparePublicKeyShare(p1, p2 *keys.PublicKeyShare) error { + if p1.PublicKey.Equal(p2.PublicKey) != 1 { + return fmt.Errorf("Expected equality on PublicKey:\n\t%s\n\t%s\n", p1.PublicKey.Hex(), p2.PublicKey.Hex()) + } + + if p1.ID != p2.ID { + return fmt.Errorf("Expected equality on ID:\n\t%d\n\t%d\n", p1.ID, p2.ID) + } + + if p1.Group != p2.Group { + return fmt.Errorf("Expected equality on Group:\n\t%v\n\t%v\n", p1.Group, p2.Group) + } + + if len(p1.Commitment) != len(p2.Commitment) { + return fmt.Errorf( + "Expected equality on Commitment length:\n\t%d\n\t%d\n", + len(p1.Commitment), + len(p1.Commitment), + ) + } + + for i := range p1.Commitment { + if p1.Commitment[i].Equal(p2.Commitment[i]) != 1 { + return fmt.Errorf( + "Expected equality on Commitment %d:\n\t%s\n\t%s\n", + i, + p1.Commitment[i].Hex(), + p1.Commitment[i].Hex(), + ) + } + } + + return nil +} + +func compareKeyShares(s1, s2 *keys.KeyShare) error { + if s1.Secret.Equal(s2.Secret) != 1 { + return fmt.Errorf("Expected equality on Secret:\n\t%s\n\t%s\n", s1.Secret.Hex(), s2.Secret.Hex()) + } + + if s1.GroupPublicKey.Equal(s2.GroupPublicKey) != 1 { + return fmt.Errorf( + "Expected equality on GroupPublicKey:\n\t%s\n\t%s\n", + s1.GroupPublicKey.Hex(), + s2.GroupPublicKey.Hex(), + ) + } + + return comparePublicKeyShare(s1.Public(), s2.Public()) +} + +func compareCommitments(c1, c2 *frost.Commitment) error { + if c1.Group != c2.Group { + return errors.New("different groups") + } + + if c1.SignerID != c2.SignerID { + return errors.New("different SignerID") + } + + if c1.CommitmentID != c2.CommitmentID { + return errors.New("different CommitmentID") + } + + if c1.HidingNonceCommitment.Equal(c2.HidingNonceCommitment) != 1 { + return errors.New("different HidingNonceCommitment") + } + + if c1.BindingNonceCommitment.Equal(c2.BindingNonceCommitment) != 1 { + return errors.New("different BindingNonceCommitment") + } + + return nil +} + +func compareNonceCommitments(c1, c2 *frost.Nonce) error { + if c1.HidingNonce.Equal(c2.HidingNonce) != 1 { + return errors.New("different HidingNonce") + } + + if c1.BindingNonce.Equal(c2.BindingNonce) != 1 { + return errors.New("different BindingNonce") + } + + return compareCommitments(c1.Commitment, c2.Commitment) +} + +func compareLambdaRegistries(t *testing.T, m1, m2 map[string]*group.Scalar) { + if len(m1) != len(m2) { + t.Fatalf("unequal lengths: %d / %d", len(m1), len(m2)) + } + + for k1, v1 := range m1 { + v2, exists := m2[k1] + if !exists { + t.Fatalf("key %s is not present in second map", k1) + } + + if v1.Equal(v2) != 1 { + t.Fatalf("unequal lambdas for the participant list %s", k1) + } + } +} + +func compareSigners(t *testing.T, s1, s2 *frost.Signer) { + if err := compareKeyShares(s1.KeyShare, s2.KeyShare); err != nil { + t.Fatal(err) + } + + compareLambdaRegistries(t, s1.LambdaRegistry, s2.LambdaRegistry) + + if len(s1.NonceCommitments) != len(s2.NonceCommitments) { + t.Fatal("expected equality") + } + + for id, com := range s1.NonceCommitments { + if com2, exists := s2.NonceCommitments[id]; !exists { + t.Fatalf("com id %d does not exist in s2", id) + } else { + if err := compareNonceCommitments(com, com2); err != nil { + t.Fatal(err) + } + } + } + + if bytes.Compare(s1.HidingRandom, s2.HidingRandom) != 0 { + t.Fatal("expected equality") + } + + if bytes.Compare(s1.BindingRandom, s2.BindingRandom) != 0 { + t.Fatal("expected equality") + } + + compareConfigurations(t, s1.Configuration, s2.Configuration, true) +} + +func compareSignatureShares(t *testing.T, s1, s2 *frost.SignatureShare) { + if s1.Group != s2.Group { + t.Fatal("unexpected group") + } + + if s1.SignerIdentifier != s2.SignerIdentifier { + t.Fatal("expected equality") + } + + if s1.SignatureShare.Equal(s2.SignatureShare) != 1 { + t.Fatal("expected equality") + } +} + +func compareSignatures(s1, s2 *frost.Signature, expectEqual bool) error { + if s1.R.Equal(s2.R) == 1 != expectEqual { + return fmt.Errorf("expected %v R", expectEqual) + } + + if s1.Z.Equal(s2.Z) == 1 != expectEqual { + return fmt.Errorf("expected %v Z", expectEqual) + } + + return nil +} + +func TestEncoding_Configuration(t *testing.T) { + testAll(t, func(t *testing.T, test *tableTest) { + configuration := makeConf(t, test) + encoded := configuration.Encode() + + decoded := new(frost.Configuration) + if err := decoded.Decode(encoded); err != nil { + t.Fatal(err) + } + + compareConfigurations(t, configuration, decoded, true) + }) +} + +func TestEncoding_Configuration_InvalidHeaderLength(t *testing.T) { + expectedError := internal.ErrInvalidLength + + testAll(t, func(t *testing.T, test *tableTest) { + configuration := makeConf(t, test) + encoded := configuration.Encode() + + decoded := new(frost.Configuration) + if err := decoded.Decode(encoded[:24]); err == nil || err.Error() != expectedError.Error() { + t.Fatalf("expected %q, got %q", expectedError, err) + } + }) +} + +func TestEncoding_Configuration_InvalidCiphersuite(t *testing.T) { + expectedError := internal.ErrInvalidCiphersuite + + testAll(t, func(t *testing.T, test *tableTest) { + configuration := makeConf(t, test) + encoded := configuration.Encode() + encoded[0] = 2 + + decoded := new(frost.Configuration) + if err := decoded.Decode(encoded); err == nil || err.Error() != expectedError.Error() { + t.Fatalf("expected %q, got %q", expectedError, err) + } + }) +} + +func TestEncoding_Configuration_InvalidLength(t *testing.T) { + expectedError := internal.ErrInvalidLength + + testAll(t, func(t *testing.T, test *tableTest) { + configuration := makeConf(t, test) + encoded := configuration.Encode() + + decoded := new(frost.Configuration) + if err := decoded.Decode(encoded[:len(encoded)-1]); err == nil || err.Error() != expectedError.Error() { + t.Fatalf("expected %q, got %q", expectedError, err) + } + + encoded = append(encoded, []byte{0, 1}...) + if err := decoded.Decode(encoded); err == nil || err.Error() != expectedError.Error() { + t.Fatalf("expected %q, got %q", expectedError, err) + } + }) +} + +func TestEncoding_Configuration_InvalidConfigEncoding(t *testing.T) { + expectedErrorPrefix := "the threshold in the encoded configuration is higher than the number of maximum participants" + tt := &tableTest{ + Ciphersuite: frost.Ristretto255, + threshold: 2, + maxSigners: 3, + } + configuration := makeConf(t, tt) + configuration.Threshold = configuration.MaxSigners + 1 + encoded := configuration.Encode() + + decoded := new(frost.Configuration) + if err := decoded.Decode(encoded); err == nil || !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } +} + +func TestEncoding_Configuration_InvalidGroupPublicKey(t *testing.T) { + expectedErrorPrefix := "could not decode group public key: element Decode: " + + testAll(t, func(t *testing.T, test *tableTest) { + configuration := makeConf(t, test) + g := group.Group(test.Ciphersuite) + encoded := configuration.Encode() + bad := badElement(t, g) + encoded = slices.Replace(encoded, 25, 25+g.ElementLength(), bad...) + + decoded := new(frost.Configuration) + if err := decoded.Decode(encoded); err == nil || !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } + }) +} + +func TestEncoding_Configuration_BadPublicKeyShare(t *testing.T) { + expectedErrorPrefix := "could not decode signer public key share for signer 1: " + + testAll(t, func(t *testing.T, test *tableTest) { + keyShares, groupPublicKey, _ := debug.TrustedDealerKeygen( + test.Ciphersuite, + nil, + test.threshold, + test.maxSigners, + ) + publicKeyShares := getPublicKeyShares(keyShares) + + configuration := &frost.Configuration{ + Ciphersuite: test.Ciphersuite, + Threshold: test.threshold, + MaxSigners: test.maxSigners, + GroupPublicKey: groupPublicKey, + SignerPublicKeyShares: publicKeyShares, + } + g := group.Group(test.Ciphersuite) + pksSize := len(publicKeyShares[0].Encode()) + bad := badElement(t, g) + offset := 25 + g.ElementLength() + pksSize + 1 + 8 + 4 + encoded := configuration.Encode() + encoded = slices.Replace(encoded, offset, offset+g.ElementLength(), bad...) + + decoded := new(frost.Configuration) + if err := decoded.Decode(encoded); err == nil || !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } + }) +} + +func TestEncoding_Configuration_InvalidPublicKeyShares(t *testing.T) { + expectedErrorPrefix := "invalid number of public keys (lower than threshold or above maximum)" + + testAll(t, func(t *testing.T, test *tableTest) { + keyShares, groupPublicKey, _ := debug.TrustedDealerKeygen( + test.Ciphersuite, + nil, + test.threshold, + test.maxSigners, + ) + publicKeyShares := getPublicKeyShares(keyShares) + + configuration := &frost.Configuration{ + Ciphersuite: test.Ciphersuite, + Threshold: test.threshold, + MaxSigners: test.maxSigners, + GroupPublicKey: groupPublicKey, + SignerPublicKeyShares: publicKeyShares, + } + configuration.SignerPublicKeyShares = configuration.SignerPublicKeyShares[:test.threshold-1] + encoded := configuration.Encode() + + decoded := new(frost.Configuration) + if err := decoded.Decode(encoded); err == nil || !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } + }) +} + +func TestEncoding_Configuration_CantVerify_InvalidPubKey(t *testing.T) { + expectedErrorPrefix := "invalid group public key, the key is the group generator (base element)" + + testAll(t, func(t *testing.T, test *tableTest) { + configuration := makeConf(t, test) + configuration.GroupPublicKey.Base() + encoded := configuration.Encode() + + decoded := new(frost.Configuration) + if err := decoded.Decode(encoded); err == nil || !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } + }) +} + +func TestEncoding_Signer(t *testing.T) { + testAll(t, func(t *testing.T, test *tableTest) { + s := makeSigners(t, test)[1] + s.Commit() + s.Commit() + + participants := make([]uint64, test.maxSigners) + for i := range test.maxSigners { + participants[i] = i + 1 + } + + s.LambdaRegistry.New(test.ECGroup(), s.Identifier(), participants) + s.LambdaRegistry.New(test.ECGroup(), s.Identifier(), participants[1:]) + + encoded := s.Encode() + + decoded := new(frost.Signer) + if err := decoded.Decode(encoded); err != nil { + t.Fatal(err) + } + + compareSigners(t, s, decoded) + }) +} + +func TestEncoding_Signer_BadConfHeader(t *testing.T) { + expectedErr := internal.ErrInvalidLength + + testAll(t, func(t *testing.T, test *tableTest) { + s := makeSigners(t, test)[0] + encoded := s.Encode() + + decoded := new(frost.Signer) + if err := decoded.Decode(encoded[:20]); err == nil || err.Error() != expectedErr.Error() { + t.Fatalf("expected error %q, got %q", expectedErr, err) + } + }) +} + +func TestEncoding_Signer_BadConf(t *testing.T) { + expectedErrorPrefix := "could not decode group public key:" + + testAll(t, func(t *testing.T, test *tableTest) { + s := makeSigners(t, test)[0] + eLen := s.Configuration.Ciphersuite.ECGroup().ElementLength() + encoded := s.Encode() + encoded = slices.Replace(encoded, 25, 25+eLen, badElement(t, test.ECGroup())...) + + decoded := new(frost.Signer) + if err := decoded.Decode(encoded); err == nil || + !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } + }) +} + +func TestEncoding_Signer_InvalidLength1(t *testing.T) { + expectedErr := internal.ErrInvalidLength + + testAll(t, func(t *testing.T, test *tableTest) { + s := makeSigners(t, test)[0] + encoded := s.Encode() + eLen := s.Configuration.Ciphersuite.ECGroup().ElementLength() + pksLen := 1 + 8 + 4 + eLen + int(test.threshold)*eLen + confLen := 1 + 3*8 + eLen + int(test.maxSigners)*pksLen + + decoded := new(frost.Signer) + if err := decoded.Decode(encoded[:confLen+2]); err == nil || err.Error() != expectedErr.Error() { + t.Fatalf("expected error %q, got %q", expectedErr, err) + } + }) +} + +func TestEncoding_Signer_InvalidLength2(t *testing.T) { + expectedErr := internal.ErrInvalidLength + + testAll(t, func(t *testing.T, test *tableTest) { + s := makeSigners(t, test)[0] + encoded := s.Encode() + + decoded := new(frost.Signer) + if err := decoded.Decode(encoded[:len(encoded)-1]); err == nil || err.Error() != expectedErr.Error() { + t.Fatalf("expected error %q, got %q", expectedErr, err) + } + + if err := decoded.Decode(append(encoded, []byte{0}...)); err == nil || err.Error() != expectedErr.Error() { + t.Fatalf("expected error %q, got %q", expectedErr, err) + } + }) +} + +func TestEncoding_Signer_InvalidLambda(t *testing.T) { + expectedErrorPrefix := "failed to decode lambda registry in signer:" + + testAll(t, func(t *testing.T, test *tableTest) { + signers := makeSigners(t, test) + + g := group.Group(test.Ciphersuite) + message := []byte("message") + + coms := make(frost.CommitmentList, len(signers)) + for i, s := range signers { + coms[i] = s.Commit() + } + + s := signers[0] + + _, err := s.Sign(message, coms) + if err != nil { + t.Fatal(err) + } + + confLen := len(s.Configuration.Encode()) + ksLen := len(s.KeyShare.Encode()) + encoded := s.Encode() + bad := badScalar(t, g) + offset := confLen + 6 + ksLen + 32 + encoded = slices.Replace(encoded, offset, offset+g.ScalarLength(), bad...) + + decoded := new(frost.Signer) + if err := decoded.Decode(encoded); err == nil || !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } + }) +} + +func TestEncoding_Signer_BadKeyShare(t *testing.T) { + expectedErrorPrefix := "failed to decode key share: invalid group identifier" + + testAll(t, func(t *testing.T, test *tableTest) { + s := makeSigners(t, test)[0] + confLen := len(s.Configuration.Encode()) + offset := confLen + 6 + + // Set an invalid group in the key share encoding. + encoded := s.Encode() + encoded = slices.Replace(encoded, offset, offset+1, []byte{2}...) + + decoded := new(frost.Signer) + if err := decoded.Decode(encoded); err == nil || !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } + }) +} + +func TestEncoding_Signer_InvalidKeyShare(t *testing.T) { + expectedErrorPrefix := "invalid key share: invalid identifier for public key share, the identifier is 0" + + testAll(t, func(t *testing.T, test *tableTest) { + s := makeSigners(t, test)[0] + confLen := len(s.Configuration.Encode()) + offset := confLen + 6 + 1 + + // Set an invalid identifier. + encoded := s.Encode() + badID := [8]byte{} + encoded = slices.Replace(encoded, offset, offset+8, badID[:]...) + + decoded := new(frost.Signer) + if err := decoded.Decode(encoded); err == nil || !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } + }) +} + +func TestEncoding_Signer_InvalidCommitmentNonces_DuplicateID(t *testing.T) { + expectedErrorPrefix := "multiple encoded commitments with the same id:" + + testAll(t, func(t *testing.T, test *tableTest) { + s := makeSigners(t, test)[0] + s.Commit() + s.Commit() + s.Commit() + g := group.Group(test.Ciphersuite) + sLen := g.ScalarLength() + confLen := len(s.Configuration.Encode()) + keyShareLen := len(s.KeyShare.Encode()) + commitmentLength := 1 + 8 + 8 + 2*uint64(g.ElementLength()) + nonceCommitmentLength := 8 + 2*sLen + int(commitmentLength) + offset := confLen + 6 + keyShareLen + offset2 := offset + nonceCommitmentLength + + encoded := s.Encode() + data := slices.Replace(encoded, offset2, offset2+8, encoded[offset:offset+8]...) + + decoded := new(frost.Signer) + if err := decoded.Decode(data); err == nil || !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } + }) +} + +func TestEncoding_Signer_InvalidHidingNonceCommitment(t *testing.T) { + expectedErrorPrefix := "can't decode hiding nonce for commitment" + + testAll(t, func(t *testing.T, test *tableTest) { + s := makeSigners(t, test)[0] + s.Commit() + g := group.Group(test.Ciphersuite) + confLen := len(s.Configuration.Encode()) + keyShareLen := len(s.KeyShare.Encode()) + offset := confLen + 6 + keyShareLen + 8 + + encoded := s.Encode() + data := slices.Replace(encoded, offset, offset+g.ScalarLength(), badScalar(t, g)...) + + decoded := new(frost.Signer) + if err := decoded.Decode(data); err == nil || !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } + }) +} + +func TestEncoding_Signer_InvalidBindingNonceCommitment(t *testing.T) { + expectedErrorPrefix := "can't decode binding nonce for commitment" + + testAll(t, func(t *testing.T, test *tableTest) { + s := makeSigners(t, test)[0] + s.Commit() + g := group.Group(test.Ciphersuite) + confLen := len(s.Configuration.Encode()) + keyShareLen := len(s.KeyShare.Encode()) + offset := confLen + 6 + keyShareLen + 8 + g.ScalarLength() + + encoded := s.Encode() + data := slices.Replace(encoded, offset, offset+g.ScalarLength(), badScalar(t, g)...) + + decoded := new(frost.Signer) + if err := decoded.Decode(data); err == nil || !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } + }) +} + +func TestEncoding_Signer_InvalidCommitment(t *testing.T) { + expectedErrorPrefix := "can't decode nonce commitment" + + testAll(t, func(t *testing.T, test *tableTest) { + s := makeSigners(t, test)[0] + s.Commit() + g := group.Group(test.Ciphersuite) + sLen := g.ScalarLength() + confLen := len(s.Configuration.Encode()) + keyShareLen := len(s.KeyShare.Encode()) + offset := confLen + 6 + keyShareLen + 8 + 2*sLen + + encoded := s.Encode() + encoded[offset] = 0 + + decoded := new(frost.Signer) + if err := decoded.Decode(encoded); err == nil || !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } + }) +} + +func TestEncoding_SignatureShare(t *testing.T) { + message := []byte("message") + + testAll(t, func(t *testing.T, test *tableTest) { + signers := makeSigners(t, test) + coms := make(frost.CommitmentList, len(signers)) + for i, s := range signers { + coms[i] = s.Commit() + } + + for _, s := range signers { + sigShare, err := s.Sign(message, coms) + if err != nil { + t.Fatal(err) + } + + encoded := sigShare.Encode() + + decoded := new(frost.SignatureShare) + if err = decoded.Decode(encoded); err != nil { + t.Fatalf("unexpected error %q", err) + } + + compareSignatureShares(t, sigShare, decoded) + } + }) +} + +func TestEncoding_SignatureShare_InvalidCiphersuite(t *testing.T) { + expectedError := internal.ErrInvalidCiphersuite + + encoded := make([]byte, 3) + + decoded := new(frost.SignatureShare) + if err := decoded.Decode(encoded); err == nil || err.Error() != expectedError.Error() { + t.Fatalf("expected %q, got %q", expectedError, err) + } +} + +func TestEncoding_SignatureShare_InvalidLength1(t *testing.T) { + expectedError := internal.ErrInvalidLength + + decoded := new(frost.SignatureShare) + if err := decoded.Decode([]byte{}); err == nil || err.Error() != expectedError.Error() { + t.Fatalf("expected %q, got %q", expectedError, err) + } +} + +func TestEncoding_SignatureShare_InvalidLength2(t *testing.T) { + expectedError := internal.ErrInvalidLength + + decoded := new(frost.SignatureShare) + if err := decoded.Decode([]byte{1, 0, 0}); err == nil || err.Error() != expectedError.Error() { + t.Fatalf("expected %q, got %q", expectedError, err) + } +} + +func TestEncoding_SignatureShare_InvalidIdentifier(t *testing.T) { + // todo: check for zero id in all decodings + expectedError := errors.New("identifier cannot be 0") + encoded := make([]byte, 41) + encoded[0] = 1 + + decoded := new(frost.SignatureShare) + if err := decoded.Decode(encoded); err == nil || err.Error() != expectedError.Error() { + t.Fatalf("expected %q, got %q", expectedError, err) + } +} + +func TestEncoding_SignatureShare_InvalidShare(t *testing.T) { + expectedErrorPrefix := "failed to decode signature share: " + message := []byte("message") + + testAll(t, func(t *testing.T, test *tableTest) { + signers := makeSigners(t, test) + coms := make(frost.CommitmentList, len(signers)) + for i, s := range signers { + coms[i] = s.Commit() + } + + s := signers[0] + + sigShare, err := s.Sign(message, coms) + if err != nil { + t.Fatal(err) + } + + encoded := sigShare.Encode() + slices.Replace(encoded, 9, 9+test.ECGroup().ScalarLength(), badScalar(t, test.ECGroup())...) + + decoded := new(frost.SignatureShare) + if err := decoded.Decode(encoded); err == nil || !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } + }) +} + +func TestEncoding_Signature(t *testing.T) { + message := []byte("message") + + testAll(t, func(t *testing.T, test *tableTest) { + key := test.ECGroup().NewScalar().Random() + signature, err := debug.Sign(test.Ciphersuite, message, key) + if err != nil { + t.Fatal(err) + } + + encoded := signature.Encode() + + decoded := new(frost.Signature) + if err = decoded.Decode(test.Ciphersuite, encoded); err != nil { + t.Fatal(err) + } + + if err = compareSignatures(signature, decoded, true); err != nil { + t.Fatal(err) + } + }) +} + +func TestEncoding_Signature_InvalidCiphersuite(t *testing.T) { + decoded := new(frost.Signature) + if err := decoded.Decode(0, nil); err == nil || err.Error() != internal.ErrInvalidCiphersuite.Error() { + t.Fatalf("expected %q, got %q", internal.ErrInvalidCiphersuite, err) + } +} + +func TestEncoding_Signature_InvalidLength(t *testing.T) { + decoded := new(frost.Signature) + if err := decoded.Decode(1, make([]byte, 63)); err == nil || err.Error() != internal.ErrInvalidLength.Error() { + t.Fatalf("expected %q, got %q", internal.ErrInvalidLength, err) + } +} + +func TestEncoding_Signature_InvalidR(t *testing.T) { + expectedErrorPrefix := "invalid signature - decoding R:" + message := []byte("message") + + testAll(t, func(t *testing.T, test *tableTest) { + key := test.ECGroup().NewScalar().Random() + signature, err := debug.Sign(test.Ciphersuite, message, key) + if err != nil { + t.Fatal(err) + } + + encoded := signature.Encode() + slices.Replace( + encoded, + 0, + test.Ciphersuite.ECGroup().ElementLength(), + badElement(t, test.Ciphersuite.ECGroup())...) + + decoded := new(frost.Signature) + if err := decoded.Decode(test.Ciphersuite, encoded); err == nil || + !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } + }) +} + +func TestEncoding_Signature_InvalidZ(t *testing.T) { + expectedErrorPrefix := "invalid signature - decoding Z:" + message := []byte("message") + + testAll(t, func(t *testing.T, test *tableTest) { + key := test.ECGroup().NewScalar().Random() + signature, err := debug.Sign(test.Ciphersuite, message, key) + if err != nil { + t.Fatal(err) + } + + encoded := signature.Encode() + g := test.Ciphersuite.ECGroup() + eLen := g.ElementLength() + sLen := g.ScalarLength() + slices.Replace(encoded, eLen, eLen+sLen, badScalar(t, g)...) + + decoded := new(frost.Signature) + if err := decoded.Decode(test.Ciphersuite, encoded); err == nil || + !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } + }) +} + +func TestEncoding_Commitment(t *testing.T) { + testAll(t, func(t *testing.T, test *tableTest) { + signer := makeSigners(t, test)[0] + com := signer.Commit() + encoded := com.Encode() + + decoded := new(frost.Commitment) + if err := decoded.Decode(encoded); err != nil { + t.Fatal(err) + } + + if err := compareCommitments(com, decoded); err != nil { + t.Fatal(err) + } + }) +} + +func TestEncoding_Commitment_BadCiphersuite(t *testing.T) { + expectedErrorPrefix := internal.ErrInvalidCiphersuite.Error() + + testAll(t, func(t *testing.T, test *tableTest) { + signer := makeSigners(t, test)[0] + com := signer.Commit() + encoded := com.Encode() + encoded[0] = 0 + + decoded := new(frost.Commitment) + if err := decoded.Decode(encoded); err == nil || !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } + }) +} + +func TestEncoding_Commitment_InvalidLength1(t *testing.T) { + expectedErrorPrefix := "failed to decode commitment: invalid length" + + testAll(t, func(t *testing.T, test *tableTest) { + signer := makeSigners(t, test)[0] + com := signer.Commit() + encoded := com.Encode() + + decoded := new(frost.Commitment) + if err := decoded.Decode(encoded[:16]); err == nil || err.Error() != expectedErrorPrefix { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } + }) +} + +func TestEncoding_Commitment_InvalidLength2(t *testing.T) { + expectedErrorPrefix := "failed to decode commitment: invalid length" + + testAll(t, func(t *testing.T, test *tableTest) { + signer := makeSigners(t, test)[0] + com := signer.Commit() + encoded := com.Encode() + + decoded := new(frost.Commitment) + if err := decoded.Decode(encoded[:35]); err == nil || err.Error() != expectedErrorPrefix { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } + }) +} + +func TestEncoding_Commitment_InvalidIdentifier(t *testing.T) { + expectedErrorPrefix := "identifier cannot be 0" + + testAll(t, func(t *testing.T, test *tableTest) { + signer := makeSigners(t, test)[0] + com := signer.Commit() + com.SignerID = 0 + encoded := com.Encode() + + decoded := new(frost.Commitment) + if err := decoded.Decode(encoded); err == nil || !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } + }) +} + +func TestEncoding_Commitment_InvalidHidingNonce(t *testing.T) { + expectedErrorPrefix := "invalid encoding of hiding nonce commitment: " + + testAll(t, func(t *testing.T, test *tableTest) { + signer := makeSigners(t, test)[0] + com := signer.Commit() + encoded := com.Encode() + bad := badElement(t, test.ECGroup()) + slices.Replace(encoded, 17, 17+test.ECGroup().ElementLength(), bad...) + + decoded := new(frost.Commitment) + if err := decoded.Decode(encoded); err == nil || !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } + }) +} + +func TestEncoding_Commitment_InvalidBindingNonce(t *testing.T) { + expectedErrorPrefix := "invalid encoding of binding nonce commitment: " + + testAll(t, func(t *testing.T, test *tableTest) { + signer := makeSigners(t, test)[0] + com := signer.Commit() + encoded := com.Encode() + g := test.ECGroup() + bad := badElement(t, g) + slices.Replace(encoded, 17+g.ElementLength(), 17+2*g.ElementLength(), bad...) + + decoded := new(frost.Commitment) + if err := decoded.Decode(encoded); err == nil || !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } + }) +} + +func TestEncoding_CommitmentList(t *testing.T) { + testAll(t, func(t *testing.T, test *tableTest) { + signers := makeSigners(t, test) + coms := make(frost.CommitmentList, len(signers)) + for i, s := range signers { + coms[i] = s.Commit() + } + + encoded := coms.Encode() + + list, err := frost.DecodeList(encoded) + if err != nil { + t.Fatal(err) + } + + if len(list) != len(coms) { + t.Fatalf("want %d, got %d", len(coms), len(list)) + } + + for i, com := range coms { + if err = compareCommitments(com, list[i]); err != nil { + t.Fatal(err) + } + } + }) +} + +func TestEncoding_CommitmentList_Empty(t *testing.T) { + com := frost.CommitmentList{} + if out := com.Encode(); out != nil { + t.Fatal("unexpected output") + } +} + +func TestEncoding_CommitmentList_InvalidCiphersuite(t *testing.T) { + expectedErrorPrefix := internal.ErrInvalidCiphersuite.Error() + + testAll(t, func(t *testing.T, test *tableTest) { + signers := makeSigners(t, test) + coms := make(frost.CommitmentList, len(signers)) + for i, s := range signers { + coms[i] = s.Commit() + } + + encoded := coms.Encode() + encoded[0] = 0 + + if _, err := frost.DecodeList(encoded); err == nil || + !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } + }) +} + +func TestEncoding_CommitmentList_InvalidLength1(t *testing.T) { + expectedErrorPrefix := internal.ErrInvalidLength.Error() + + testAll(t, func(t *testing.T, test *tableTest) { + signers := makeSigners(t, test) + coms := make(frost.CommitmentList, len(signers)) + for i, s := range signers { + coms[i] = s.Commit() + } + + encoded := coms.Encode() + + if _, err := frost.DecodeList(encoded[:8]); err == nil || + !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } + }) +} + +func TestEncoding_CommitmentList_InvalidLength2(t *testing.T) { + expectedErrorPrefix := internal.ErrInvalidLength.Error() + + testAll(t, func(t *testing.T, test *tableTest) { + signers := makeSigners(t, test) + coms := make(frost.CommitmentList, len(signers)) + for i, s := range signers { + coms[i] = s.Commit() + } + + encoded := coms.Encode() + + if _, err := frost.DecodeList(encoded[:9]); err == nil || + !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } + }) +} + +func TestEncoding_CommitmentList_InvalidCommitment(t *testing.T) { + expectedErrorPrefix := "invalid encoding of commitment: " + internal.ErrInvalidCiphersuite.Error() + + testAll(t, func(t *testing.T, test *tableTest) { + signers := makeSigners(t, test) + coms := make(frost.CommitmentList, len(signers)) + for i, s := range signers { + coms[i] = s.Commit() + } + + encoded := coms.Encode() + encoded[9] = 0 + + if _, err := frost.DecodeList(encoded); err == nil || + !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } + }) +} + +func TestEncoding_KeyShare_Bytes(t *testing.T) { + testAll(t, func(t *testing.T, test *tableTest) { + s := makeSigners(t, test)[0] + keyShare := s.KeyShare + + encoded := keyShare.Encode() + + decoded := new(keys.KeyShare) + if err := decoded.Decode(encoded); err != nil { + t.Fatal(err) + } + + if err := compareKeyShares(keyShare, decoded); err != nil { + t.Fatal(err) + } + }) +} + +func TestEncoding_KeyShare_JSON(t *testing.T) { + testAll(t, func(t *testing.T, test *tableTest) { + s := makeSigners(t, test)[0] + keyShare := s.KeyShare + + encoded, err := json.Marshal(keyShare) + if err != nil { + t.Fatal(err) + } + + decoded := new(keys.KeyShare) + if err := json.Unmarshal(encoded, decoded); err != nil { + t.Fatal(err) + } + + if err := compareKeyShares(keyShare, decoded); err != nil { + t.Fatal(err) + } + + // expect error + decoded = new(keys.KeyShare) + expectedError := errors.New("invalid group identifier") + encoded = replaceStringInBytes(encoded, fmt.Sprintf("\"group\":%d", test.ECGroup()), "\"group\":70") + + if err := json.Unmarshal(encoded, decoded); err == nil || err.Error() != expectedError.Error() { + t.Fatalf("expected error %q, got %q", expectedError, err) + } + }) +} + +func TestEncoding_PublicKeyShare_Bytes(t *testing.T) { + testAll(t, func(t *testing.T, test *tableTest) { + s := makeSigners(t, test)[0] + keyShare := s.KeyShare.Public() + + encoded := keyShare.Encode() + + decoded := new(keys.PublicKeyShare) + if err := decoded.Decode(encoded); err != nil { + t.Fatal(err) + } + + if err := comparePublicKeyShare(keyShare, decoded); err != nil { + t.Fatal(err) + } + }) +} + +func TestEncoding_PublicKeyShare_JSON(t *testing.T) { + testAll(t, func(t *testing.T, test *tableTest) { + s := makeSigners(t, test)[0] + keyShare := s.KeyShare.Public() + + encoded, err := json.Marshal(keyShare) + if err != nil { + t.Fatal(err) + } + + decoded := new(keys.PublicKeyShare) + if err := json.Unmarshal(encoded, decoded); err != nil { + t.Fatal(err) + } + + if err := comparePublicKeyShare(keyShare, decoded); err != nil { + t.Fatal(err) + } + + // expect error + decoded = new(keys.PublicKeyShare) + expectedError := errors.New("invalid group identifier") + encoded = replaceStringInBytes(encoded, fmt.Sprintf("\"group\":%d", test.ECGroup()), "\"group\":70") + + if err := json.Unmarshal(encoded, decoded); err == nil || err.Error() != expectedError.Error() { + t.Fatalf("expected error %q, got %q", expectedError, err) + } + }) +} diff --git a/tests/frost_error_test.go b/tests/frost_error_test.go new file mode 100644 index 0000000..eb6a74a --- /dev/null +++ b/tests/frost_error_test.go @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: MIT +// +// Copyright (C) 2024 Daniel Bourdrez. All Rights Reserved. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree or at +// https://spdx.org/licenses/MIT.html + +package frost_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/bytemare/frost" + "github.com/bytemare/frost/internal" +) + +func TestMaliciousSigner(t *testing.T) { +} + +func TestVerifySignature_BadCiphersuite(t *testing.T) { + expectedErrorPrefix := internal.ErrInvalidCiphersuite + + if err := frost.VerifySignature(2, nil, nil, nil); err == nil || + !strings.HasPrefix(err.Error(), expectedErrorPrefix.Error()) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } +} + +func TestVerifySignature_InvalidSignature(t *testing.T) { + expectedErrorPrefix := "invalid Signature" + message := []byte("message") + + testAll(t, func(t *testing.T, test *tableTest) { + configuration, _ := fullSetup(t, test) + + signature := &frost.Signature{ + R: test.ECGroup().Base(), + Z: test.ECGroup().NewScalar().Random(), + } + + if err := frost.VerifySignature(test.Ciphersuite, message, signature, configuration.GroupPublicKey); err == nil || + !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } + }) +} + +func TestSigner_Sign_NoNonceForCommitmentID(t *testing.T) { + message := []byte("message") + tt := &tableTest{ + Ciphersuite: frost.Ristretto255, + threshold: 2, + maxSigners: 3, + } + + signers := makeSigners(t, tt) + + coms := make(frost.CommitmentList, len(signers)) + for i, s := range signers { + coms[i] = s.Commit() + } + + coms[0].CommitmentID = 0 + expectedErrorPrefix := fmt.Sprintf( + "the commitment identifier %d for signer %d in the commitments is unknown to the signer", + coms[0].CommitmentID, + coms[0].SignerID, + ) + + if _, err := signers[0].Sign(message, coms); err == nil || + !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } +} + +/* +func TestSigner_Sign_FailedLambdaGeneration(t *testing.T) { + call signer.Sign +} + +func TestSigner_Sign_VerifyCommitmentList_BadCommitment(t *testing.T) { + call signer.Sign +} + +func TestSigner_Sign_VerifyCommitmentList_NoCommitmentForSigner(t *testing.T) { + call signer.Sign +} + +func TestSigner_Sign_VerifyNonces_BadCommitmentID(t *testing.T) { + +} + +func TestSigner_Sign_VerifyNonces_BadHidingNonceCommitment(t *testing.T) { + +} + +func TestSigner_Sign_VerifyNonces_BadBindingNonceCommitment(t *testing.T) { + +} + +*/ diff --git a/tests/frost_test.go b/tests/frost_test.go index 9887495..72fa12d 100644 --- a/tests/frost_test.go +++ b/tests/frost_test.go @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT // -// Copyright (C) 2023 Daniel Bourdrez. All Rights Reserved. +// Copyright (C) 2024 Daniel Bourdrez. All Rights Reserved. // // This source code is licensed under the MIT license found in the // LICENSE file in the root directory of this source tree or at @@ -9,181 +9,143 @@ package frost_test import ( + "fmt" "testing" group "github.com/bytemare/crypto" - "github.com/bytemare/hash" - secretsharing "github.com/bytemare/secret-sharing" "github.com/bytemare/frost" - "github.com/bytemare/frost/internal" + "github.com/bytemare/frost/debug" + "github.com/bytemare/frost/keys" ) -var configurationTable = []frost.Configuration{ +type tableTest struct { + frost.Ciphersuite + threshold, maxSigners uint64 +} + +var testTable = []tableTest{ { - GroupPublicKey: nil, - Ciphersuite: internal.Ciphersuite{ - ContextString: []byte("FROST-ED25519-SHA512-v1"), - Hash: hash.SHA512, - Group: group.Edwards25519Sha512, - }, + Ciphersuite: frost.Ed25519, + threshold: 2, + maxSigners: 3, }, { - Ciphersuite: internal.Ciphersuite{ - Group: group.Ristretto255Sha512, - Hash: hash.SHA512, - ContextString: []byte("FROST-RISTRETTO255-SHA512-v1"), - }, - GroupPublicKey: nil, + Ciphersuite: frost.Ristretto255, + threshold: 2, + maxSigners: 3, }, { - Ciphersuite: internal.Ciphersuite{ - Group: group.P256Sha256, - Hash: hash.SHA256, - ContextString: []byte("FROST-P256-SHA256-v1"), - }, - GroupPublicKey: nil, + Ciphersuite: frost.P256, + threshold: 2, + maxSigners: 3, }, { - GroupPublicKey: nil, - Ciphersuite: internal.Ciphersuite{ - ContextString: []byte("FROST-secp256k1-SHA256-v1"), - Hash: hash.SHA256, - Group: group.Secp256k1, - }, + Ciphersuite: frost.Secp256k1, + threshold: 2, + maxSigners: 3, }, } -func TestTrustedDealerKeygen(t *testing.T) { - min := 2 - max := 3 - - testAll(t, func(t2 *testing.T, configuration *frost.Configuration) { - g := configuration.Ciphersuite.Group +func runFrost( + t *testing.T, + test *tableTest, + threshold, maxSigners uint64, + message []byte, + keyShares []*keys.KeyShare, + groupPublicKey *group.Element, +) { + // Collect public keys. + publicKeyShares := getPublicKeyShares(keyShares) + + // Set up configuration. + configuration := &frost.Configuration{ + Ciphersuite: test.Ciphersuite, + Threshold: threshold, + MaxSigners: maxSigners, + GroupPublicKey: groupPublicKey, + SignerPublicKeyShares: publicKeyShares, + } - groupSecretKey := g.NewScalar().Random() + if err := configuration.Init(); err != nil { + panic(err) + } - privateKeyShares, dealerGroupPubKey, secretsharingCommitment, err := frost.TrustedDealerKeygen( - g, - groupSecretKey, - max, - min, - ) + // Create Participants + participants := make(ParticipantList, threshold) + for i, ks := range keyShares[:threshold] { + signer, err := configuration.Signer(ks) if err != nil { - t.Fatal(err) + panic(err) } - if len(secretsharingCommitment) != min { - t2.Fatalf("%d / %d", len(secretsharingCommitment), min) - } + participants[i] = signer + } - recoveredKey, err := secretsharing.Combine(g, uint(min), privateKeyShares) - if err != nil { - t.Fatal(err) - } + // Commit + commitments := make(frost.CommitmentList, threshold) + for i, p := range participants { + commitments[i] = p.Commit() + } - if recoveredKey.Equal(groupSecretKey) != 1 { - t.Fatal() - } + commitments.Sort() - groupPublicKey, participantPublicKeys := frost.DeriveGroupInfo(g, max, secretsharingCommitment) - if len(participantPublicKeys) != max { - t2.Fatal() + // Sign + sigShares := make([]*frost.SignatureShare, threshold) + for i, p := range participants { + var err error + sigShares[i], err = p.Sign(message, commitments) + if err != nil { + t.Fatal(err) } + } - if groupPublicKey.Equal(dealerGroupPubKey) != 1 { - t2.Fatal() - } + // Aggregate + _, err := configuration.AggregateSignatures(message, sigShares, commitments, true) + if err != nil { + t.Fatal(err) + } - configuration.GroupPublicKey = dealerGroupPubKey + // Sanity Check + groupSecretKey, err := debug.RecoverGroupSecret(test.Ciphersuite, keyShares) + if err != nil { + t.Fatal(err) + } - for i, shareI := range privateKeyShares { - if !frost.VerifyVSS(g, shareI, secretsharingCommitment) { - t2.Fatal(i) - } - } + singleSig, err := debug.Sign(test.Ciphersuite, message, groupSecretKey) + if err != nil { + t.Fatal(err) + } - pkEnc := groupPublicKey.Encode() + if err = frost.VerifySignature(test.Ciphersuite, message, singleSig, groupPublicKey); err != nil { + t.Fatal(err) + } +} - recoveredPK := g.NewElement() - if err := recoveredPK.Decode(pkEnc); err != nil { - t2.Fatal(err) - } +func TestFrost_WithTrustedDealer(t *testing.T) { + message := []byte("test") - if recoveredPK.Equal(groupPublicKey) != 1 { - t2.Fatal() - } + testAll(t, func(t *testing.T, test *tableTest) { + g := test.Ciphersuite.ECGroup() + sk := g.NewScalar().Random() + keyShares, groupPublicKey, _ := debug.TrustedDealerKeygen(test.Ciphersuite, sk, test.threshold, test.maxSigners) + runFrost(t, test, test.threshold, test.maxSigners, message, keyShares, groupPublicKey) }) } -func TestFrost(t *testing.T) { - max := 3 - threshold := 2 - participantListInt := []int{1, 3} +func TestFrost_WithDKG(t *testing.T) { message := []byte("test") - testAll(t, func(t2 *testing.T, configuration *frost.Configuration) { - g := configuration.Ciphersuite.Group - - privateKeyShares, _, groupPublicKey := SimulateDKG(configuration, max, threshold) - configuration.GroupPublicKey = groupPublicKey - - // Create Participants - participants := make(ParticipantList, max) - for i, share := range privateKeyShares { - participants[i] = configuration.Participant(share.Identifier, share.SecretKey) - } - - signatureAggregator := &frost.Participant{ - Configuration: *configuration, - } - - // Round One: Commitment - participantList := make([]*group.Scalar, len(participantListInt)) - for i, p := range participantListInt { - participantList[i] = internal.IntegerToScalar(g, p) - } - - comList := make(frost.CommitmentList, len(participantList)) - for i, id := range participantList { - p := participants.Get(id) - comList[i] = p.Commit() - } - - comList.Sort() - - // Round Two: Sign - sigShares := make([]*frost.SignatureShare, len(participantList)) - for i, id := range participantList { - p := participants.Get(id) - - sigShare, err := p.Sign(message, comList) - if err != nil { - t.Fatal(err) - } - - sigShares[i] = sigShare - } - - // Final step: aggregate - _ = signatureAggregator.Aggregate(comList, message, sigShares) - - // Sanity Check - groupSecretKey, err := secretsharing.Combine(g, uint(threshold), privateKeyShares) - if err != nil { - t.Fatal(err) - } - - singleSig := frost.Sign(configuration.Ciphersuite, message, groupSecretKey) - if !frost.Verify(configuration.Ciphersuite, message, singleSig, groupPublicKey) { - t2.Fatal() - } + testAll(t, func(t *testing.T, test *tableTest) { + g := test.Ciphersuite.ECGroup() + keyShares, groupPublicKey, _ := runDKG(t, g, test.threshold, test.maxSigners) + runFrost(t, test, test.threshold, test.maxSigners, message, keyShares, groupPublicKey) }) } -func testAll(t *testing.T, f func(*testing.T, *frost.Configuration)) { - for _, test := range configurationTable { - t.Run(string(test.Ciphersuite.ContextString), func(t *testing.T) { +func testAll(t *testing.T, f func(*testing.T, *tableTest)) { + for _, test := range testTable { + t.Run(fmt.Sprintf("%s", test.ECGroup()), func(t *testing.T) { f(t, &test) }) } diff --git a/tests/misc_test.go b/tests/misc_test.go new file mode 100644 index 0000000..12769f2 --- /dev/null +++ b/tests/misc_test.go @@ -0,0 +1,401 @@ +// SPDX-License-Identifier: MIT +// +// Copyright (C) 2024 Daniel Bourdrez. All Rights Reserved. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree or at +// https://spdx.org/licenses/MIT.html + +package frost_test + +import ( + "errors" + "fmt" + "strings" + "testing" + + group "github.com/bytemare/crypto" + "github.com/bytemare/dkg" + + "github.com/bytemare/frost" + "github.com/bytemare/frost/debug" + "github.com/bytemare/frost/internal" + "github.com/bytemare/frost/keys" +) + +func verifyTrustedDealerKeygen( + t *testing.T, + test *tableTest, + ks []*keys.KeyShare, + pk *group.Element, + coms []*group.Element, +) { + if uint64(len(coms)) != test.threshold { + t.Fatalf("%d / %d", len(coms), test.threshold) + } + + recoveredKey, err := debug.RecoverGroupSecret(test.Ciphersuite, ks[:test.threshold]) + if err != nil { + t.Fatal(err) + } + + groupPublicKey, participantPublicKeys, err := debug.RecoverPublicKeys( + test.Ciphersuite, + test.maxSigners, + coms, + ) + if err != nil { + t.Fatal(err) + } + + if uint64(len(participantPublicKeys)) != test.maxSigners { + t.Fatal() + } + + if groupPublicKey.Equal(pk) != 1 { + t.Fatal() + } + + g := test.Ciphersuite.ECGroup() + + for i, shareI := range ks { + if !debug.VerifyVSS(g, shareI, coms) { + t.Fatal(i) + } + } + + sig, err := debug.Sign(test.Ciphersuite, []byte("message"), recoveredKey) + if err != nil { + t.Fatal(err) + } + + if err = frost.VerifySignature(test.Ciphersuite, []byte("message"), sig, groupPublicKey); err != nil { + t.Fatal(err) + } +} + +func TestTrustedDealerKeygen(t *testing.T) { + testAll(t, func(t *testing.T, test *tableTest) { + g := test.Ciphersuite.ECGroup() + groupSecretKey := g.NewScalar().Random() + keyShares, dealerGroupPubKey, secretsharingCommitment := debug.TrustedDealerKeygen( + test.Ciphersuite, + groupSecretKey, + test.threshold, + test.maxSigners, + ) + + verifyTrustedDealerKeygen(t, test, keyShares, dealerGroupPubKey, secretsharingCommitment) + }) +} + +func TestTrustedDealerKeygenNoSecret(t *testing.T) { + testAll(t, func(t *testing.T, test *tableTest) { + keyShares, dealerGroupPubKey, secretsharingCommitment := debug.TrustedDealerKeygen( + test.Ciphersuite, + nil, + test.threshold, + test.maxSigners, + ) + + verifyTrustedDealerKeygen(t, test, keyShares, dealerGroupPubKey, secretsharingCommitment) + }) +} + +func TestTrustedDealerKeygenWrongParams(t *testing.T) { + errTooFewShares := errors.New("number of shares must be equal or greater than the threshold") + + testAll(t, func(t *testing.T, test *tableTest) { + if err := testPanic("wrong params", errTooFewShares, func() { + _, _, _ = debug.TrustedDealerKeygen( + test.Ciphersuite, + nil, + 5, + 3, + ) + }); err != nil { + t.Fatal(err) + } + }) +} + +func TestRecoverGroupSecretInvalidCiphersuite(t *testing.T) { + expectedError := internal.ErrInvalidCiphersuite + if _, err := debug.RecoverGroupSecret(0, nil); err == nil || err.Error() != expectedError.Error() { + t.Fatalf("expected %q, got %q", expectedError, err) + } +} + +func TestRecoverGroupSecretNoShares(t *testing.T) { + expectedError := "failed to reconstruct group secret: " + if _, err := debug.RecoverGroupSecret(frost.Ristretto255, nil); err == nil || + !strings.HasPrefix(err.Error(), expectedError) { + t.Fatalf("expected %q, got %q", expectedError, err) + } +} + +func TestSchnorrSign(t *testing.T) { + message1 := []byte("message-1") + message2 := []byte("message-2") + testAll(t, func(t *testing.T, test *tableTest) { + g := test.ECGroup() + secretKey1 := g.NewScalar().Random() + verificationKey1 := g.Base().Multiply(secretKey1) + secretKey2 := g.NewScalar().Random() + + // Signature must be valid. + signature, err := debug.Sign(test.Ciphersuite, message1, secretKey1) + if err != nil { + t.Fatal(err) + } + + if err = frost.VerifySignature(test.Ciphersuite, message1, signature, verificationKey1); err != nil { + t.Fatal(err) + } + + // Same key, different messages = different signatures + signature1, err := debug.Sign(test.Ciphersuite, message1, secretKey1) + if err != nil { + t.Fatal(err) + } + + signature2, err := debug.Sign(test.Ciphersuite, message2, secretKey1) + if err != nil { + t.Fatal(err) + } + + if err = compareSignatures(signature1, signature2, false); err != nil { + t.Fatal(err) + } + + // Same key, same message = different signatures + signature1, err = debug.Sign(test.Ciphersuite, message1, secretKey1) + if err != nil { + t.Fatal(err) + } + + signature2, err = debug.Sign(test.Ciphersuite, message1, secretKey1) + if err != nil { + t.Fatal(err) + } + + if err = compareSignatures(signature1, signature2, false); err != nil { + t.Fatal(err) + } + + // Same key, same message, same random = same signatures + k := g.NewScalar().Random() + + signature1, err = debug.Sign(test.Ciphersuite, message1, secretKey1, k) + if err != nil { + t.Fatal(err) + } + + signature2, err = debug.Sign(test.Ciphersuite, message1, secretKey1, k) + if err != nil { + t.Fatal(err) + } + + if err = compareSignatures(signature1, signature2, true); err != nil { + t.Fatal(err) + } + + // Same key, same message, explicit different random = same signatures + k1 := g.NewScalar().Random() + k2 := g.NewScalar().Random() + + if k1.Equal(k2) == 1 { + t.Fatal("unexpected equality") + } + + signature1, err = debug.Sign(test.Ciphersuite, message1, secretKey1, k1) + if err != nil { + t.Fatal(err) + } + + signature2, err = debug.Sign(test.Ciphersuite, message1, secretKey1, k2) + if err != nil { + t.Fatal(err) + } + + if err = compareSignatures(signature1, signature2, false); err != nil { + t.Fatal(err) + } + + // Different keys, same message = different signatures + signature1, err = debug.Sign(test.Ciphersuite, message1, secretKey1) + if err != nil { + t.Fatal(err) + } + + signature2, err = debug.Sign(test.Ciphersuite, message1, secretKey2) + if err != nil { + t.Fatal(err) + } + + if err = compareSignatures(signature1, signature2, false); err != nil { + t.Fatal(err) + } + + // Different keys, different messages = different signatures + signature1, err = debug.Sign(test.Ciphersuite, message1, secretKey1) + if err != nil { + t.Fatal(err) + } + + signature2, err = debug.Sign(test.Ciphersuite, message2, secretKey2) + if err != nil { + t.Fatal(err) + } + + if err = compareSignatures(signature1, signature2, false); err != nil { + t.Fatal(err) + } + }) +} + +func TestSign_InvalidCiphersuite(t *testing.T) { + expectedError := internal.ErrInvalidCiphersuite + if _, err := debug.Sign(0, nil, nil); err == nil || err.Error() != expectedError.Error() { + t.Fatalf("expected %q, got %q", expectedError, err) + } +} + +func TestRecoverPublicKeys(t *testing.T) { + testAll(t, func(t *testing.T, test *tableTest) { + keyShares, dealerGroupPubKey, secretsharingCommitment := debug.TrustedDealerKeygen( + test.Ciphersuite, + nil, + test.threshold, + test.maxSigners, + ) + + groupPublicKey, participantPublicKeys, err := debug.RecoverPublicKeys( + test.Ciphersuite, + test.maxSigners, + secretsharingCommitment, + ) + if err != nil { + t.Fatal(err) + } + + if dealerGroupPubKey.Equal(groupPublicKey) != 1 { + t.Fatal("expected equality") + } + + if len(participantPublicKeys) != len(keyShares) { + t.Fatal("expected equality") + } + + for i, keyShare := range keyShares { + if keyShare.PublicKey.Equal(participantPublicKeys[i]) != 1 { + t.Fatal("expected equality") + } + } + }) +} + +func TestRecoverPublicKeys_InvalidCiphersuite(t *testing.T) { + expectedError := internal.ErrInvalidCiphersuite + if _, _, err := debug.RecoverPublicKeys(0, 0, nil); err == nil || err.Error() != expectedError.Error() { + t.Fatalf("expected %q, got %q", expectedError, err) + } +} + +func TestRecoverPublicKeys_BadCommitment(t *testing.T) { + expectedError := "can't recover public keys: commitment has nil element" + ciphersuite := frost.Ristretto255 + threshold := uint64(2) + maxSigners := uint64(3) + _, _, secretsharingCommitment := debug.TrustedDealerKeygen( + ciphersuite, + nil, + threshold, + maxSigners, + ) + + secretsharingCommitment[1] = nil + + _, _, err := debug.RecoverPublicKeys( + ciphersuite, + maxSigners, + secretsharingCommitment, + ) + if err == nil || err.Error() != expectedError { + t.Fatalf("expected %q, got %q", expectedError, err) + } +} + +func TestPublicKeyShareVerification(t *testing.T) { + testAll(t, func(t *testing.T, test *tableTest) { + keyShares, dealerGroupPubKey, _ := runDKG( + t, + test.Ciphersuite.ECGroup(), + test.threshold, + test.maxSigners, + ) + + vssComs := make([][]*group.Element, test.maxSigners) + pkShares := make([]*keys.PublicKeyShare, test.maxSigners) + + for i, keyShare := range keyShares { + pk := keyShare.Public() + vssComs[i] = pk.Commitment + pkShares[i] = pk + } + + if err := dkg.VerifyPublicKey(dkg.Ciphersuite(test.Ciphersuite), 0, dealerGroupPubKey, vssComs); err != nil { + t.Fatal(err) + } + + for _, pk := range pkShares { + if !pk.Verify(vssComs) { + t.Fatal("expected validity") + } + } + }) +} + +func TestPublicKeyShareVerificationFail(t *testing.T) { + testAll(t, func(t *testing.T, test *tableTest) { + keyShares, dealerGroupPubKey, _ := runDKG( + t, + test.Ciphersuite.ECGroup(), + test.threshold, + test.maxSigners, + ) + + vssComs := make([][]*group.Element, test.maxSigners) + pkShares := make([]*keys.PublicKeyShare, test.maxSigners) + + for i, keyShare := range keyShares { + pk := keyShare.Public() + vssComs[i] = pk.Commitment + pk.PublicKey = nil + pkShares[i] = pk + } + + if err := dkg.VerifyPublicKey(dkg.Ciphersuite(test.Ciphersuite), 0, dealerGroupPubKey, vssComs); err != nil { + t.Fatal(err) + } + + for _, pk := range pkShares { + if pk.Verify(vssComs) { + t.Fatal("expected invalidity") + } + } + }) +} + +func TestLambda_BadID(t *testing.T) { + // expectedErrorPrefix := "anomaly in participant identifiers: one of the polynomial's coefficients is zero" + g := group.Ristretto255Sha512 + polynomial := []*group.Scalar{ + g.NewScalar().SetUInt64(1), + g.NewScalar().SetUInt64(2), + g.NewScalar().SetUInt64(3), + } + + // todo : what happens if the participant list is not vetted? + fmt.Println(internal.Lambda(g, 4, polynomial).Hex()) +} diff --git a/tests/signer_test.go b/tests/signer_test.go new file mode 100644 index 0000000..7601658 --- /dev/null +++ b/tests/signer_test.go @@ -0,0 +1,186 @@ +// SPDX-License-Identifier: MIT +// +// Copyright (C) 2024 Daniel Bourdrez. All Rights Reserved. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree or at +// https://spdx.org/licenses/MIT.html + +package frost_test + +import ( + "fmt" + "strings" + "testing" + + group "github.com/bytemare/crypto" + + "github.com/bytemare/frost" + "github.com/bytemare/frost/internal" +) + +func TestLambdaRegistry(t *testing.T) { + g := group.Ristretto255Sha512 + id := uint64(2) + participants := []uint64{1, 2, 3, 4} + lambdas := make(internal.LambdaRegistry) + + // Get should return nil + if lambda := lambdas.Get(participants); lambda != nil { + t.Fatal("unexpected result") + } + + // Create a new entry + lambda := lambdas.New(g, id, participants) + + if lambda == nil { + t.Fatal("unexpected result") + } + + // Getting the same entry + lambda2 := lambdas.Get(participants) + if lambda.Equal(lambda2) != 1 { + t.Fatal("expected equality") + } + + lambda3 := lambdas.GetOrNew(g, id, participants) + + if lambda.Equal(lambda3) != 1 { + t.Fatal("expected equality") + } + + // Getting another entry must result in another returned value + lambda4 := lambdas.GetOrNew(g, id, participants[:3]) + + if lambda.Equal(lambda4) == 1 { + t.Fatal("unexpected equality") + } + + lambda5 := lambdas.GetOrNew(g, id, participants[:3]) + + if lambda4.Equal(lambda5) != 1 { + t.Fatal("expected equality") + } + + // Removing and checking for the same entry + lambdas.Delete(participants) + if lambda = lambdas.Get(participants); lambda != nil { + t.Fatal("unexpected result") + } + + // Setting must return the same value + lambda6 := g.NewScalar().Random() + lambdas.Set(participants, lambda6) + lambda7 := lambdas.Get(participants) + + if lambda6.Equal(lambda7) != 1 { + t.Fatal("expected equality") + } +} + +func TestSigner_VerifyCommitmentList_InvalidCommitmentList(t *testing.T) { + expectedErrorPrefix := "invalid list of commitments: commitment list is empty" + tt := &tableTest{ + Ciphersuite: frost.Ristretto255, + threshold: 2, + maxSigners: 3, + } + _, signers := fullSetup(t, tt) + + if err := signers[0].VerifyCommitmentList(nil); err == nil || + !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } +} + +func TestSigner_VerifyCommitmentList_MissingCommitment(t *testing.T) { + expectedErrorPrefix := "signer identifier 1 not found in the commitment list" + tt := &tableTest{ + Ciphersuite: frost.Ristretto255, + threshold: 2, + maxSigners: 3, + } + _, signers := fullSetup(t, tt) + coms := make(frost.CommitmentList, len(signers)) + + for i, s := range signers { + coms[i] = s.Commit() + } + + if err := signers[0].VerifyCommitmentList(coms[1:]); err == nil || + !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } +} + +func TestSigner_VerifyCommitmentList_MissingNonce(t *testing.T) { + tt := &tableTest{ + Ciphersuite: frost.Ristretto255, + threshold: 2, + maxSigners: 3, + } + _, signers := fullSetup(t, tt) + coms := make(frost.CommitmentList, len(signers)) + + for i, s := range signers { + coms[i] = s.Commit() + } + + delete(signers[0].NonceCommitments, coms[0].CommitmentID) + expectedErrorPrefix := fmt.Sprintf( + "the commitment identifier %d for signer %d in the commitments is unknown to the signer", + coms[0].CommitmentID, + coms[0].SignerID, + ) + + if err := signers[0].VerifyCommitmentList(coms); err == nil || + !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } +} + +func TestSigner_VerifyCommitmentList_BadHidingNonce(t *testing.T) { + expectedErrorPrefix := "invalid hiding nonce in commitment list for signer 1" + + tt := &tableTest{ + Ciphersuite: frost.Ristretto255, + threshold: 2, + maxSigners: 3, + } + _, signers := fullSetup(t, tt) + coms := make(frost.CommitmentList, len(signers)) + + for i, s := range signers { + coms[i] = s.Commit() + } + + signers[0].NonceCommitments[coms[0].CommitmentID].HidingNonceCommitment.Base() + + if err := signers[0].VerifyCommitmentList(coms); err == nil || + !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } +} + +func TestSigner_VerifyCommitmentList_BadBindingNonce(t *testing.T) { + expectedErrorPrefix := "invalid binding nonce in commitment list for signer 1" + + tt := &tableTest{ + Ciphersuite: frost.Ristretto255, + threshold: 2, + maxSigners: 3, + } + _, signers := fullSetup(t, tt) + coms := make(frost.CommitmentList, len(signers)) + + for i, s := range signers { + coms[i] = s.Commit() + } + + signers[0].NonceCommitments[coms[0].CommitmentID].BindingNonceCommitment.Base() + + if err := signers[0].VerifyCommitmentList(coms); err == nil || + !strings.HasPrefix(err.Error(), expectedErrorPrefix) { + t.Fatalf("expected %q, got %q", expectedErrorPrefix, err) + } +} diff --git a/tests/utils_test.go b/tests/utils_test.go new file mode 100644 index 0000000..04acce9 --- /dev/null +++ b/tests/utils_test.go @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: MIT +// +// Copyright (C) 2024 Daniel Bourdrez. All Rights Reserved. +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree or at +// https://spdx.org/licenses/MIT.html + +package frost_test + +import ( + "bytes" + "errors" + "fmt" + "math/big" + "slices" + "strings" + "testing" + + group "github.com/bytemare/crypto" + + "github.com/bytemare/frost/internal" +) + +var ( + errNoPanic = errors.New("no panic") + errNoPanicMessage = errors.New("panic but no message") +) + +func hasPanic(f func()) (has bool, err error) { + defer func() { + var report any + if report = recover(); report != nil { + has = true + err = fmt.Errorf("%v", report) + } + }() + + f() + + return has, err +} + +// testPanic executes the function f with the expectation to recover from a panic. If no panic occurred or if the +// panic message is not the one expected, testPanic returns an error. +func testPanic(s string, expectedError error, f func()) error { + hasPanic, err := hasPanic(f) + + // if there was no panic + if !hasPanic { + return errNoPanic + } + + // panic, and we don't expect a particular message + if expectedError == nil { + return nil + } + + // panic, but the panic value is empty + if err == nil { + return errNoPanicMessage + } + + // panic, but the panic value is not what we expected + if err.Error() != expectedError.Error() { + return fmt.Errorf("expected panic on %s with message %q, got %q", s, expectedError, err) + } + + return nil +} + +func badScalar(t *testing.T, g group.Group) []byte { + order, ok := new(big.Int).SetString(g.Order(), 0) + if !ok { + t.Errorf("setting int in base %d failed: %v", 0, g.Order()) + } + + encoded := make([]byte, g.ScalarLength()) + order.FillBytes(encoded) + + if g == group.Ristretto255Sha512 || g == group.Edwards25519Sha512 { + slices.Reverse(encoded) + } + + return encoded +} + +func badElement(t *testing.T, g group.Group) []byte { + order, ok := new(big.Int).SetString(g.Order(), 0) + if !ok { + t.Errorf("setting int in base %d failed: %v", 0, g.Order()) + } + + encoded := make([]byte, g.ElementLength()) + order.FillBytes(encoded) + + if g == group.Ristretto255Sha512 || g == group.Edwards25519Sha512 { + slices.Reverse(encoded) + } + + return encoded +} + +func expectError(expectedError error, f func() error) error { + if err := f(); err == nil || err.Error() != expectedError.Error() { + return fmt.Errorf("expected %q, got %q", expectedError, err) + } + + return nil +} + +func expectErrorPrefix(expectedErrorMessagePrefix string, f func() error) error { + if err := f(); err == nil || !strings.HasPrefix(err.Error(), expectedErrorMessagePrefix) { + return fmt.Errorf("expected error prefix %q, got %q", expectedErrorMessagePrefix, err) + } + + return nil +} + +func replaceStringInBytes(data []byte, old, new string) []byte { + s := string(data) + s = strings.Replace(s, old, new, 1) + + return []byte(s) +} + +func TestConcatenate(t *testing.T) { + inputs := [][]byte{ + {1, 2, 3}, + {4, 5, 6}, + {7, 8, 9}, + } + + var nilSlice []byte + + // nil + if !bytes.Equal(internal.Concatenate(), slices.Concat(nilSlice)) { + t.Fatal("expected equality") + } + + // empty + if !bytes.Equal(internal.Concatenate([]byte{}), slices.Concat([]byte{})) { + t.Fatal("expected equality") + } + + // using single input + if !bytes.Equal(internal.Concatenate(inputs[0]), slices.Concat(internal.Concatenate(inputs[0]))) { + t.Fatal("expected equality") + } + + // using multiple input + if !bytes.Equal(internal.Concatenate(inputs...), slices.Concat(internal.Concatenate(inputs...))) { + t.Fatal("expected equality") + } +} diff --git a/tests/vector_utils_test.go b/tests/vector_utils_test.go index e6d3ebd..98cc87f 100644 --- a/tests/vector_utils_test.go +++ b/tests/vector_utils_test.go @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT // -// Copyright (C) 2023 Daniel Bourdrez. All Rights Reserved. +// Copyright (C) 2024 Daniel Bourdrez. All Rights Reserved. // // This source code is licensed under the MIT license found in the // LICENSE file in the root directory of this source tree or at @@ -19,14 +19,14 @@ import ( secretsharing "github.com/bytemare/secret-sharing" "github.com/bytemare/frost" - "github.com/bytemare/frost/internal" + "github.com/bytemare/frost/keys" ) -type ParticipantList []*frost.Participant +type ParticipantList []*frost.Signer -func (p ParticipantList) Get(id *group.Scalar) *frost.Participant { +func (p ParticipantList) Get(id uint64) *frost.Signer { for _, i := range p { - if i.ParticipantInfo.KeyShare.Identifier.Equal(id) == 1 { + if i.KeyShare.ID == id { return i } } @@ -34,13 +34,13 @@ func (p ParticipantList) Get(id *group.Scalar) *frost.Participant { return nil } -func stringToInt(t *testing.T, s string) int { - i, err := strconv.ParseInt(s, 10, 32) +func stringToUint(t *testing.T, s string) uint { + i, err := strconv.ParseUint(s, 10, 0) if err != nil { t.Fatal(err) } - return int(i) + return uint(i) } func decodeScalar(t *testing.T, g group.Group, enc []byte) *group.Scalar { @@ -84,7 +84,7 @@ func (j *ByteToHex) UnmarshalJSON(b []byte) error { */ type testVectorInput struct { - ParticipantList []int `json:"participant_list"` + ParticipantList []uint64 `json:"participant_list"` GroupSecretKey ByteToHex `json:"group_secret_key"` GroupPublicKey ByteToHex `json:"group_public_key"` Message ByteToHex `json:"message"` @@ -112,18 +112,19 @@ type testVectorConfig struct { } func (c testVectorConfig) decode(t *testing.T) *testConfig { + threshold := stringToUint(t, c.MinParticipants) + maxSigners := stringToUint(t, c.MaxParticipants) + return &testConfig{ - MaxParticipants: stringToInt(t, c.MaxParticipants), - NumParticipants: stringToInt(t, c.NumParticipants), - MinParticipants: stringToInt(t, c.MinParticipants), Name: c.Name, - Configuration: configToConfiguration(t, &c), + NumParticipants: stringToUint(t, c.NumParticipants), + Configuration: configToConfiguration(t, &c, threshold, maxSigners), } } type testVectorParticipantShare struct { ParticipantShare ByteToHex `json:"participant_share"` - Identifier int `json:"identifier"` + Identifier uint64 `json:"identifier"` } type testParticipant struct { @@ -135,7 +136,7 @@ type testParticipant struct { BindingNonceCommitment ByteToHex `json:"binding_nonce_commitment"` BindingFactorInput ByteToHex `json:"binding_factor_input"` BindingFactor ByteToHex `json:"binding_factor"` - Identifier int `json:"identifier"` + Identifier uint64 `json:"identifier"` } type testVectorRoundOneOutputs struct { @@ -144,7 +145,7 @@ type testVectorRoundOneOutputs struct { type testVectorSigShares struct { SigShare ByteToHex `json:"sig_share"` - Identifier int `json:"identifier"` + Identifier uint64 `json:"identifier"` } type testVectorRoundTwoOutputs struct { @@ -159,18 +160,16 @@ type testConfig struct { *frost.Configuration Name string ContextString []byte - MaxParticipants int - NumParticipants int - MinParticipants int + NumParticipants uint } type testInput struct { - ParticipantList []*group.Scalar + ParticipantList []uint64 GroupSecretKey *group.Scalar GroupPublicKey *group.Element Message []byte SharePolynomialCoefficients []*group.Scalar - Participants []*secretsharing.KeyShare + Participants []*keys.KeyShare } type test struct { @@ -182,7 +181,6 @@ type test struct { } type participant struct { - ID *group.Scalar HidingNonce *group.Scalar BindingNonce *group.Scalar HidingNonceCommitment *group.Element @@ -191,6 +189,7 @@ type participant struct { HidingNonceRandomness []byte BindingNonceRandomness []byte BindingFactorInput []byte + ID uint64 } type testRoundOneOutputs struct { @@ -198,23 +197,33 @@ type testRoundOneOutputs struct { } type testRoundTwoOutputs struct { - Outputs []*secretsharing.KeyShare + Outputs []*frost.SignatureShare } /* Parsing and decoding functions. */ -func configToConfiguration(t *testing.T, c *testVectorConfig) *frost.Configuration { +func makeFrostConfig(c frost.Ciphersuite, threshold, maxSigners uint) *frost.Configuration { + return &frost.Configuration{ + Ciphersuite: c, + Threshold: uint64(threshold), + MaxSigners: uint64(maxSigners), + GroupPublicKey: nil, + SignerPublicKeyShares: nil, + } +} + +func configToConfiguration(t *testing.T, c *testVectorConfig, threshold, maxSigners uint) *frost.Configuration { switch c.Group { case "ed25519": - return frost.Ed25519.Configuration() + return makeFrostConfig(frost.Ed25519, threshold, maxSigners) case "ristretto255": - return frost.Ristretto255.Configuration() + return makeFrostConfig(frost.Ristretto255, threshold, maxSigners) case "P-256": - return frost.P256.Configuration() + return makeFrostConfig(frost.P256, threshold, maxSigners) case "secp256k1": - return frost.Secp256k1.Configuration() + return makeFrostConfig(frost.Secp256k1, threshold, maxSigners) default: t.Fatalf("group not supported: %s", c.Group) } @@ -224,7 +233,7 @@ func configToConfiguration(t *testing.T, c *testVectorConfig) *frost.Configurati func decodeParticipant(t *testing.T, g group.Group, tp *testParticipant) *participant { return &participant{ - ID: internal.IntegerToScalar(g, tp.Identifier), + ID: tp.Identifier, HidingNonceRandomness: tp.HidingNonceRandomness, BindingNonceRandomness: tp.BindingNonceRandomness, HidingNonce: decodeScalar(t, g, tp.HidingNonce), @@ -241,23 +250,32 @@ func (i testVectorInput) decode(t *testing.T, g group.Group) *testInput { GroupSecretKey: decodeScalar(t, g, i.GroupSecretKey), GroupPublicKey: decodeElement(t, g, i.GroupPublicKey), Message: i.Message, - SharePolynomialCoefficients: make([]*group.Scalar, len(i.SharePolynomialCoefficients)), - Participants: make([]*secretsharing.KeyShare, len(i.ParticipantShares)), - ParticipantList: make([]*group.Scalar, len(i.ParticipantList)), + SharePolynomialCoefficients: make([]*group.Scalar, len(i.SharePolynomialCoefficients)+1), + Participants: make([]*keys.KeyShare, len(i.ParticipantShares)), + ParticipantList: make([]uint64, len(i.ParticipantList)), } for j, id := range i.ParticipantList { - input.ParticipantList[j] = internal.IntegerToScalar(g, id) + input.ParticipantList[j] = id } + input.SharePolynomialCoefficients[0] = input.GroupSecretKey for j, coeff := range i.SharePolynomialCoefficients { - input.SharePolynomialCoefficients[j] = decodeScalar(t, g, coeff) + input.SharePolynomialCoefficients[j+1] = decodeScalar(t, g, coeff) } for j, p := range i.ParticipantShares { - input.Participants[j] = &secretsharing.KeyShare{ - Identifier: internal.IntegerToScalar(g, p.Identifier), - SecretKey: decodeScalar(t, g, p.ParticipantShare), + secret := decodeScalar(t, g, p.ParticipantShare) + public := g.Base().Multiply(secret) + input.Participants[j] = &keys.KeyShare{ + Secret: secret, + GroupPublicKey: input.GroupPublicKey, + PublicKeyShare: secretsharing.PublicKeyShare{ + PublicKey: public, + Commitment: nil, + ID: p.Identifier, + Group: g, + }, } } @@ -278,13 +296,13 @@ func (o testVectorRoundOneOutputs) decode(t *testing.T, g group.Group) *testRoun func (o testVectorRoundTwoOutputs) decode(t *testing.T, g group.Group) *testRoundTwoOutputs { r := &testRoundTwoOutputs{ - Outputs: make([]*secretsharing.KeyShare, len(o.Outputs)), + Outputs: make([]*frost.SignatureShare, len(o.Outputs)), } for i, p := range o.Outputs { - r.Outputs[i] = &secretsharing.KeyShare{ - Identifier: internal.IntegerToScalar(g, p.Identifier), - SecretKey: decodeScalar(t, g, p.SigShare), + r.Outputs[i] = &frost.SignatureShare{ + SignerIdentifier: p.Identifier, + SignatureShare: decodeScalar(t, g, p.SigShare), } } @@ -293,11 +311,24 @@ func (o testVectorRoundTwoOutputs) decode(t *testing.T, g group.Group) *testRoun func (v testVector) decode(t *testing.T) *test { conf := v.Config.decode(t) + inputs := v.Inputs.decode(t, conf.Ciphersuite.ECGroup()) + + conf.GroupPublicKey = inputs.GroupPublicKey + conf.SignerPublicKeyShares = make([]*keys.PublicKeyShare, len(inputs.Participants)) + + for i, ks := range inputs.Participants { + conf.SignerPublicKeyShares[i] = ks.Public() + } + + if err := conf.Configuration.Init(); err != nil { + t.Fatal(err) + } + return &test{ Config: conf, - Inputs: v.Inputs.decode(t, conf.Ciphersuite.Group), - RoundOneOutputs: v.RoundOneOutputs.decode(t, conf.Ciphersuite.Group), - RoundTwoOutputs: v.RoundTwoOutputs.decode(t, conf.Ciphersuite.Group), + Inputs: inputs, + RoundOneOutputs: v.RoundOneOutputs.decode(t, conf.Ciphersuite.ECGroup()), + RoundTwoOutputs: v.RoundTwoOutputs.decode(t, conf.Ciphersuite.ECGroup()), FinalOutput: v.FinalOutput.Sig, } } diff --git a/tests/vectors_test.go b/tests/vectors_test.go index 76b3d5f..803e757 100644 --- a/tests/vectors_test.go +++ b/tests/vectors_test.go @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT // -// Copyright (C) 2023 Daniel Bourdrez. All Rights Reserved. +// Copyright (C) 2024 Daniel Bourdrez. All Rights Reserved. // // This source code is licensed under the MIT license found in the // LICENSE file in the root directory of this source tree or at @@ -16,47 +16,30 @@ import ( "path/filepath" "testing" - secretsharing "github.com/bytemare/secret-sharing" + group "github.com/bytemare/crypto" "github.com/bytemare/frost" + "github.com/bytemare/frost/debug" + "github.com/bytemare/frost/keys" ) -func (v test) test(t *testing.T) { - g := v.Config.Ciphersuite.Group - - coeffs := v.Inputs.SharePolynomialCoefficients +func (v test) testTrustedDealer(t *testing.T) ([]*keys.KeyShare, *group.Element) { + g := v.Config.Ciphersuite.ECGroup() - privateKeyShares, dealerGroupPubKey, secretsharingCommitment, err := frost.TrustedDealerKeygen( - g, + keyShares, dealerGroupPubKey, secretsharingCommitment := debug.TrustedDealerKeygen( + v.Config.Ciphersuite, v.Inputs.GroupSecretKey, - v.Config.MaxParticipants, - v.Config.MinParticipants, - coeffs...) - if err != nil { - t.Fatal(err) - } + v.Config.Configuration.Threshold, + v.Config.Configuration.MaxSigners, + v.Inputs.SharePolynomialCoefficients...) - if len(secretsharingCommitment) != v.Config.MinParticipants { + if uint64(len(secretsharingCommitment)) != v.Config.Configuration.Threshold { t.Fatalf( - "%d / %d", len(secretsharingCommitment), v.Config.MinParticipants) - } - - // Check whether key shares are the same - cpt := len(privateKeyShares) - for _, p := range privateKeyShares { - for _, p2 := range v.Inputs.Participants { - if p2.Identifier.Equal(p.Identifier) == 1 { - cpt-- - } - } - } - - if cpt != 0 { - t.Fatal("Some key shares do not match.") + "%d / %d", len(secretsharingCommitment), v.Config.Configuration.Threshold) } // Test recovery of the full secret signing key. - recoveredKey, err := secretsharing.Combine(g, uint(v.Config.MinParticipants), privateKeyShares) + recoveredKey, err := debug.RecoverGroupSecret(v.Config.Ciphersuite, keyShares) if err != nil { t.Fatal(err) } @@ -65,8 +48,16 @@ func (v test) test(t *testing.T) { t.Fatal() } - groupPublicKey, participantPublicKey := frost.DeriveGroupInfo(g, v.Config.MaxParticipants, secretsharingCommitment) - if len(participantPublicKey) != v.Config.MaxParticipants { + groupPublicKey, participantPublicKey, err := debug.RecoverPublicKeys( + v.Config.Ciphersuite, + v.Config.Configuration.MaxSigners, + secretsharingCommitment, + ) + if err != nil { + t.Fatal(err) + } + + if uint64(len(participantPublicKey)) != v.Config.Configuration.MaxSigners { t.Fatal() } @@ -74,18 +65,42 @@ func (v test) test(t *testing.T) { t.Fatal() } - for i, shareI := range privateKeyShares { - if !frost.VerifyVSS(g, shareI, secretsharingCommitment) { + for i, shareI := range keyShares { + if !debug.VerifyVSS(g, shareI, secretsharingCommitment) { t.Fatal(i) } } + return keyShares, dealerGroupPubKey +} + +func (v test) test(t *testing.T) { + keyShares, groupPublicKey := v.testTrustedDealer(t) + + // Check whether key shares are the same + cpt := len(keyShares) + for _, p := range keyShares { + for _, p2 := range v.Inputs.Participants { + if p2.Identifier() == p.Identifier() && p2.SecretKey().Equal(p.Secret) == 1 { + cpt-- + } + } + } + + if cpt != 0 { + t.Fatal("Some key shares do not match.") + } + // Create participants - participants := make(ParticipantList, len(privateKeyShares)) + participants := make(ParticipantList, len(keyShares)) conf := v.Config - conf.GroupPublicKey = groupPublicKey - for i, pks := range privateKeyShares { - participants[i] = conf.Participant(pks.Identifier, pks.SecretKey) + for i, keyShare := range keyShares { + signer, err := conf.Configuration.Signer(keyShare) + if err != nil { + t.Fatal(err) + } + + participants[i] = signer } // Round One: Commitment @@ -98,7 +113,7 @@ func (v test) test(t *testing.T) { var pv *participant for _, pp := range v.RoundOneOutputs.Outputs { - if pp.ID.Equal(pid.ID) == 1 { + if pp.ID == pid.ID { pv = pp } } @@ -109,62 +124,60 @@ func (v test) test(t *testing.T) { p.HidingRandom = pv.HidingNonceRandomness p.BindingRandom = pv.BindingNonceRandomness - commitment := p.Commit() + com := p.Commit() - if p.Nonce[0].Equal(pv.HidingNonce) != 1 { + if p.NonceCommitments[com.CommitmentID].HidingNonce.Equal(pv.HidingNonce) != 1 { t.Fatal(i) } - if p.Nonce[1].Equal(pv.BindingNonce) != 1 { + if p.NonceCommitments[com.CommitmentID].BindingNonce.Equal(pv.BindingNonce) != 1 { t.Fatal(i) } - if commitment.HidingNonce.Equal(pv.HidingNonceCommitment) != 1 { + if com.HidingNonceCommitment.Equal(pv.HidingNonceCommitment) != 1 { t.Fatal(i) } - if commitment.BindingNonce.Equal(pv.BindingNonceCommitment) != 1 { + if com.BindingNonceCommitment.Equal(pv.BindingNonceCommitment) != 1 { t.Fatal(i) } - commitmentList[i] = commitment + commitmentList[i] = com } - //_, rhoInputs := commitmentList.ComputeBindingFactors( - // v.Config.Ciphersuite, - // v.Inputs.Message, - //) - //for i, rho := range rhoInputs { - // if !bytes.Equal(rho, v.RoundOneOutputs.Outputs[i].BindingFactorInput) { - // t.Fatal() - // } - //} - // Round two: sign sigShares := make([]*frost.SignatureShare, len(v.RoundTwoOutputs.Outputs)) - for i, pid := range v.RoundTwoOutputs.Outputs { - p := participants.Get(pid.Identifier) + for i, share := range v.RoundTwoOutputs.Outputs { + p := participants.Get(share.SignerIdentifier) if p == nil { t.Fatal(i) } + var err error sigShares[i], err = p.Sign(v.Inputs.Message, commitmentList) if err != nil { t.Fatal(err) } - } - for i, ks := range v.RoundTwoOutputs.Outputs { - if ks.SecretKey.Equal(sigShares[i].SignatureShare) != 1 { - t.Fatal(i) + // Check against vector + if share.SignatureShare.Equal(sigShares[i].SignatureShare) != 1 { + t.Fatalf("%s\n%s\n", share.SignatureShare.Hex(), sigShares[i].SignatureShare.Hex()) + } + + if err = v.Config.VerifySignatureShare(sigShares[i], v.Inputs.Message, commitmentList); err != nil { + t.Fatalf("signature share matched but verification failed: %s", err) } } - // Aggregate - sig := participants[1].Aggregate(commitmentList, v.Inputs.Message, sigShares) + // AggregateSignatures + sig, err := v.Config.AggregateSignatures(v.Inputs.Message, sigShares, commitmentList, true) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(sig.Encode(), v.FinalOutput) { - t.Fatal() + t.Fatal("") } // Sanity Check - if !frost.Verify(conf.Ciphersuite, v.Inputs.Message, sig, groupPublicKey) { + if err = frost.VerifySignature(conf.Ciphersuite, v.Inputs.Message, sig, groupPublicKey); err != nil { t.Fatal() } }