├── .github ├── .golangci.yml ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ ├── bug-report.md │ ├── config.yml │ └── enhancement.md ├── Makefile ├── PULL_REQUEST_TEMPLATE │ └── pull_request_template.md ├── SECURITY.md ├── licence-header.tmpl ├── renovate.json ├── sonar-project.properties └── workflows │ ├── wf-analysis.yaml │ └── wf-tests.yaml ├── .gitignore ├── LICENSE ├── README.md ├── client.go ├── deserializer.go ├── examples_test.go ├── go.mod ├── go.sum ├── internal ├── ake │ ├── 3dh.go │ ├── client.go │ └── server.go ├── configuration.go ├── encoding │ ├── encoding.go │ ├── i2osp.go │ └── misc.go ├── hash.go ├── keyrecovery │ ├── envelope.go │ └── keyrec.go ├── masking │ └── masking.go ├── oprf │ ├── client.go │ ├── oprf.go │ └── server.go └── tag │ └── strings.go ├── message ├── credentials.go ├── login.go └── registration.go ├── opaque.go ├── server.go └── tests ├── client_test.go ├── deserializer_test.go ├── encoding_test.go ├── fuzz_test.go ├── helper_test.go ├── opaque_test.go ├── oprfVectors.json ├── oprf_test.go ├── server_test.go ├── testdata └── fuzz │ ├── FuzzConfiguration │ ├── 9c7be5871d57b46a │ └── f3f7abd74e23d56a │ ├── FuzzDeserializeKE1 │ └── fuzzbuzz-a1ae7e291f3d65e6ffdc8acb9f39f3538e3e0ab4 │ └── FuzzDeserializeKE2 │ └── fuzzbuzz-a1ae7e291f3d65e6ffdc8acb9f39f3538e3e0ab4 ├── vectors.json └── vectors_test.go /.github/.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | default: all 4 | disable: 5 | - funcorder 6 | - nonamedreturns 7 | - varnamelen 8 | enable: 9 | - asasalint 10 | - asciicheck 11 | - bidichk 12 | - bodyclose 13 | - canonicalheader 14 | - containedctx 15 | - contextcheck 16 | - copyloopvar 17 | - cyclop 18 | - decorder 19 | - depguard 20 | - dogsled 21 | - dupl 22 | - dupword 23 | - durationcheck 24 | - err113 25 | - errcheck 26 | - errchkjson 27 | - errname 28 | - errorlint 29 | - exhaustive 30 | - exhaustruct 31 | - exptostd 32 | - fatcontext 33 | - forbidigo 34 | - forcetypeassert 35 | - funcorder 36 | - funlen 37 | - ginkgolinter 38 | - gocheckcompilerdirectives 39 | - gochecknoglobals 40 | - gochecknoinits 41 | - gochecksumtype 42 | - gocognit 43 | - goconst 44 | - gocritic 45 | - gocyclo 46 | - godot 47 | - godox 48 | - goheader 49 | - gomoddirectives 50 | - gomodguard 51 | - goprintffuncname 52 | - gosec 53 | - gosmopolitan 54 | - govet 55 | - grouper 56 | - iface 57 | - importas 58 | - inamedparam 59 | - ineffassign 60 | - interfacebloat 61 | - intrange 62 | - ireturn 63 | - lll 64 | - loggercheck 65 | - maintidx 66 | - makezero 67 | - mirror 68 | - misspell 69 | - mnd 70 | - musttag 71 | - nakedret 72 | - nestif 73 | - nilerr 74 | - nilnesserr 75 | - nilnil 76 | - nlreturn 77 | - noctx 78 | - nolintlint 79 | - nonamedreturns 80 | - nosprintfhostport 81 | - paralleltest 82 | - perfsprint 83 | - prealloc 84 | - predeclared 85 | - promlinter 86 | - protogetter 87 | - reassign 88 | - recvcheck 89 | - revive 90 | - rowserrcheck 91 | - sloglint 92 | - spancheck 93 | - sqlclosecheck 94 | - staticcheck 95 | - tagalign 96 | - tagliatelle 97 | - testableexamples 98 | - testifylint 99 | - testpackage 100 | - thelper 101 | - tparallel 102 | - unconvert 103 | - unparam 104 | - unused 105 | - usestdlibvars 106 | - usetesting 107 | - varnamelen 108 | - wastedassign 109 | - whitespace 110 | - wrapcheck 111 | - wsl 112 | - zerologlint 113 | settings: 114 | cyclop: 115 | max-complexity: 13 116 | depguard: 117 | rules: 118 | main: 119 | list-mode: lax 120 | allow: 121 | - golang.org/x/crypto/* 122 | errcheck: 123 | check-type-assertions: true 124 | check-blank: true 125 | gocritic: 126 | enable-all: true 127 | disabled-checks: 128 | - unnamedResult 129 | govet: 130 | enable-all: true 131 | settings: 132 | shadow: 133 | strict: true 134 | interfacebloat: 135 | max: 11 136 | mnd: 137 | checks: 138 | - argument 139 | - case 140 | - condition 141 | - operation 142 | - return 143 | - assign 144 | ignored-numbers: 145 | - '2' 146 | - '3' 147 | - '4' 148 | - '8' 149 | nlreturn: 150 | block-size: 2 151 | prealloc: 152 | simple: false 153 | for-loops: true 154 | 155 | exclusions: 156 | rules: 157 | - path: internal/hash.go 158 | text: "Error return value of `h.h.Write` is not checked" 159 | - path: internal/tag/strings.go 160 | text: "G101: Potential hardcoded credentials" 161 | issues: 162 | max-issues-per-linter: 0 163 | max-same-issues: 0 164 | formatters: 165 | enable: 166 | - gci 167 | - gofmt 168 | - gofumpt 169 | - goimports 170 | - golines 171 | settings: 172 | gci: 173 | sections: 174 | - standard 175 | - default 176 | - prefix(github.com/bytemare/opaque) 177 | - blank 178 | - dot 179 | - alias 180 | no-inline-comments: true 181 | no-prefix-comments: true 182 | custom-order: true 183 | goimports: 184 | local-prefixes: 185 | - github.com/bytemare/opaque 186 | golines: 187 | max-len: 200 188 | output: 189 | sort-order: 190 | - file 191 | run: 192 | tests: false -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @bytemare -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.0-4baaaa.svg)](code_of_conduct.md) 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, caste, color, religion, or sexual identity 11 | and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the 27 | overall community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or 32 | advances of any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email 36 | address, without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Attribution 41 | 42 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 43 | version 2.0, available at 44 | [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. 45 | 46 | Community Impact Guidelines were inspired by 47 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 48 | 49 | For answers to common questions about this code of conduct, see the FAQ at 50 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available 51 | at [https://www.contributor-covenant.org/translations][translations]. 52 | 53 | [homepage]: https://www.contributor-covenant.org 54 | [v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html 55 | [Mozilla CoC]: https://github.com/mozilla/diversity 56 | [FAQ]: https://www.contributor-covenant.org/faq 57 | [translations]: https://www.contributor-covenant.org/translations 58 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute to OPAQUE 2 | 3 | ### Is your contribution related to the protocol or this implementation? 4 | 5 | - If you have thoughts or questions, consider opening an issue in the [CFRG project of OPAQUE](https://github.com/cfrg/draft-irtf-cfrg-opaque) or sending an e-mail to the [mailing list](https://www.irtf.org/mailman/listinfo/cfrg). 6 | - If not sure, you can [open a new issue](https://github.com/bytemare/opaque/issues/new) here. 7 | 8 | ### Did you find a bug? 🐞 9 | 10 | * 🔎 Please ensure your findings have not already been reported by searching on the project repository under [Issues](https://github.com/bytemare/opaque). 11 | * If you think your findings can be complementary to an existing issue, don't hesitate to join the conversation 😃☕ 12 | * If there's no issue addressing the problem, [open a new one](https://github.com/bytemare/opaque/issues/new). Please be clear in the title and description, and add relevant information. Bonus points if you provide a **code sample** and everything needed to reproduce the issue when expected behaviour is not occurring. 13 | * If possible, use the relevant issue templates. 14 | 15 | ### Do you have a fix? 16 | 17 | 🎉 That's awesome! Pull requests are welcome! 18 | 19 | * Please [open an issue](https://github.com/bytemare/opaque) beforehand, so we can discuss this. 20 | * Fork this repo from `main`, and ensure your fork is up-to-date with it when submitting the PR. 21 | * If your changes impact the documentation, please update it accordingly. 22 | * If you added code that impact tests, please add tests with relevant coverage and test cases. Bonus points for fuzzing. 23 | * 🛠️ Make sure the test suite passes. 24 | 25 | If your changes might have an impact on performance, please benchmark your code and measure the impact, share the results and the tests that lead to these results. 26 | 27 | Please note that changes that are purely cosmetic and do not add anything substantial to the stability, functionality, or testability of the project may not be accepted. 28 | 29 | ### Coding Convention 30 | 31 | 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. 32 | 33 | ### Governance Model 34 | 35 | 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. 36 | 37 | ### Licence 38 | 39 | By contributing to this project, you agree that your contributions will be licensed under the project's [License](https://github.com/bytemare/opaque/blob/main/LICENSE). 40 | 41 | 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). 42 | 43 | Thanks! :heart: -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41E Bug report" 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: bytemare 7 | 8 | --- 9 | 10 | 13 | 14 | ### Describe the bug 15 | A clear and concise description of what the bug is. 16 | 17 | ### Your setup 18 | 19 | **What version/commit of the project are you using?** 20 | 21 | **What version of go are you using?** 22 |
23 | $ go version
24 | 
25 | 
26 | 27 | **What does the go environment look like?** 28 |
go env Output
29 | $ go env
30 | 
31 | 
32 | 33 | **If relevant, what parameters or arguments are you using?** 34 | 35 | ### Reproducing 36 | 37 | **What did you do?** 38 | Steps to reproduce the behavior: 39 | 1. '....' 40 | 41 | **Expected behavior** 42 | A clear and concise description of what you expected to happen. 43 | 44 | **Additional context** 45 | Add any other context about the problem here. 46 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Questions, feature requests, and more 💬 4 | url: https://github.com/bytemare/opaque/discussions 5 | about: Do you need help? Did you make something with OPAQUE? Do you have an idea? Tell us about it! -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/enhancement.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "📈 Enhancement" 3 | about: Request or discuss improvements 4 | title: "[Enhancement]" 5 | labels: enhancement 6 | assignees: bytemare 7 | 8 | --- 9 | 10 | 13 | 14 | ### Describe the feature 15 | 16 | A clear and concise description of what the enhancement is and what problem it solves. 17 | 18 | **Expected behaviour** 19 | 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Additional context** 23 | 24 | Add any other context about the problem here. 25 | -------------------------------------------------------------------------------- /.github/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: update 2 | update: 3 | @echo "Updating dependencies..." 4 | @cd ../ && go get -u ./... 5 | @go mod tidy 6 | 7 | .PHONY: update-linters 8 | update-linters: 9 | @echo "Updating linters..." 10 | @go install mvdan.cc/gofumpt@latest 11 | @go install github.com/daixiang0/gci@latest 12 | @go install github.com/segmentio/golines@latest 13 | @go install golang.org/x/tools/cmd/goimports@latest 14 | @go install golang.org/x/tools/go/analysis/passes/fieldalignment/cmd/fieldalignment@latest 15 | @curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin 16 | 17 | .PHONY: fmt 18 | fmt: 19 | @echo "Formatting ..." 20 | @go mod tidy 21 | @go fmt ../... 22 | @golines -m 120 -t 4 -w ../ 23 | @gofumpt -w -extra ../ 24 | @gci write -s Standard -s Default -s "Prefix($(shell go list -m))" ../ 25 | @fieldalignment -fix ../... 26 | 27 | .PHONY: license 28 | license: 29 | @echo "Checking License headers ..." 30 | @if addlicense -check -v -skip yaml -f licence-header.tmpl ../*; then echo "License headers OK"; else return 1; fi; 31 | 32 | .PHONY: lint 33 | lint: fmt license 34 | @echo "Linting ..." 35 | @if golangci-lint run --config=.golangci.yml ../...; then echo "Linting OK"; else return 1; fi; 36 | 37 | .PHONY: test 38 | test: 39 | @echo "Running all tests ..." 40 | @go test -v -vet=all ../... 41 | 42 | .PHONY: vectors 43 | vectors: 44 | @echo "Testing vectors ..." 45 | @go test -v ../tests/vectors_test.go 46 | 47 | .PHONY: cover 48 | cover: 49 | @echo "Testing with coverage ..." 50 | @go test -v -race -covermode=atomic -coverpkg=../... -coverprofile=./coverage.out ../tests 51 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ### Description 4 | 5 | 6 | ### Related Issue 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | ### Motivation and Context 15 | 16 | 17 | ### How Has This Been Tested? 18 | 19 | 20 | 21 | 22 | ### Types of changes 23 | 24 | - [ ] Bug fix (non-breaking change which fixes an issue) 25 | - [ ] New feature (non-breaking change which adds functionality) 26 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 27 | 28 | ### Checklist: 29 | 30 | 31 | - [ ] I have read the **CONTRIBUTING** document. 32 | - [ ] My code follows the code style of this project. 33 | - [ ] My change requires a change to the documentation. 34 | - [ ] I have updated the documentation accordingly. 35 | - [ ] I have added tests to cover my changes. 36 | - [ ] All new and existing tests passed. 37 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | The OPAQUE protocol is still in the process of specification. Therefore, this implementation evolves with the draft. 6 | Only the latest version will be benefit from security fixes. Maintainers of projects using this implementation of OPAQUE are invited to update their dependency. 7 | 8 | ## Reporting a Vulnerability 9 | 10 | Vulnerabilities can be reported through Github issues, here: https://github.com/bytemare/opaque/security/advisories 11 | If the issue is sensitive enough that the reporter thinks the discussion needs more confidentiality, we can discuss options there (e.g. On a Security Advisory or per e-mail). 12 | -------------------------------------------------------------------------------- /.github/licence-header.tmpl: -------------------------------------------------------------------------------- 1 | SPDX-License-Identifier: MIT 2 | 3 | Copyright (C) 2025 Daniel Bourdrez. All Rights Reserved. 4 | 5 | This source code is licensed under the MIT license found in the 6 | LICENSE file in the root directory of this source tree or at 7 | https://spdx.org/licenses/MIT.html -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "github>bytemare/renovate-config" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.github/sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.organization=bytemare 2 | sonar.projectKey=opaque 3 | sonar.sources=. 4 | sonar.tests=tests/ 5 | sonar.test.exclusions=examples_test.go,tests/** 6 | sonar.verbose=true 7 | sonar.coverage.exclusions=examples_test.go,tests/** 8 | sonar.go.coverage.reportPaths=coverage.out -------------------------------------------------------------------------------- /.github/workflows/wf-analysis.yaml: -------------------------------------------------------------------------------- 1 | name: Analysis 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | schedule: 11 | # Every 3 days at 7 a.m. 12 | - cron: '0 7 */3 * *' 13 | 14 | permissions: {} 15 | 16 | jobs: 17 | Lint: 18 | permissions: 19 | contents: read 20 | uses: bytemare/workflows/.github/workflows/golangci-lint.yaml@696fab4908e73675d0c90d77637ecaed7e93e978 21 | with: 22 | config-path: ./.github/.golangci.yml 23 | scope: ./... 24 | 25 | CodeQL: 26 | permissions: 27 | actions: read 28 | contents: read 29 | security-events: write 30 | uses: bytemare/workflows/.github/workflows/codeql.yaml@696fab4908e73675d0c90d77637ecaed7e93e978 31 | with: 32 | language: go 33 | 34 | CodeScans: 35 | permissions: 36 | contents: read 37 | # Needed to upload the results to code-scanning dashboard. 38 | security-events: write 39 | uses: bytemare/workflows/.github/workflows/scan-go.yaml@696fab4908e73675d0c90d77637ecaed7e93e978 40 | with: 41 | sonar-configuration: .github/sonar-project.properties 42 | secrets: 43 | github: ${{ secrets.GITHUB_TOKEN }} 44 | sonar: ${{ secrets.SONAR_TOKEN }} 45 | codecov: ${{ secrets.CODECOV_TOKEN }} 46 | semgrep: ${{ secrets.SEMGREP_APP_TOKEN }} 47 | 48 | Scorecard: 49 | permissions: 50 | # Needed to upload the results to code-scanning dashboard. 51 | security-events: write 52 | # Needed for GitHub OIDC token if publish_results is true. 53 | id-token: write 54 | # Needed for nested workflow 55 | actions: read 56 | # To detect SAST tools 57 | checks: read 58 | attestations: read 59 | contents: read 60 | deployments: read 61 | issues: read 62 | discussions: read 63 | packages: read 64 | pages: read 65 | pull-requests: read 66 | repository-projects: read 67 | statuses: read 68 | models: read 69 | 70 | uses: bytemare/workflows/.github/workflows/scorecard.yaml@696fab4908e73675d0c90d77637ecaed7e93e978 71 | secrets: 72 | token: ${{ secrets.SCORECARD_TOKEN }} 73 | -------------------------------------------------------------------------------- /.github/workflows/wf-tests.yaml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | permissions: {} 12 | 13 | jobs: 14 | Test: 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | go: [ '1.24', '1.23', '1.22' ] 19 | uses: bytemare/workflows/.github/workflows/test-go.yaml@696fab4908e73675d0c90d77637ecaed7e93e978 20 | with: 21 | version: ${{ matrix.go }} 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | .idea -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2025 Daniel Bourdrez 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OPAQUE 2 | [![OPAQUE](https://github.com/bytemare/opaque/actions/workflows/wf-analysis.yaml/badge.svg)](https://github.com/bytemare/opaque/actions/workflows/wf-analysis.yaml) 3 | [![Go Reference](https://pkg.go.dev/badge/github.com/bytemare/opaque.svg)](https://pkg.go.dev/github.com/bytemare/opaque) 4 | [![codecov](https://codecov.io/gh/bytemare/opaque/branch/main/graph/badge.svg?token=5bQfB0OctA)](https://codecov.io/gh/bytemare/opaque) 5 | 6 | ``` 7 | import "github.com/bytemare/opaque" 8 | ``` 9 | 10 | This package implements [OPAQUE](https://datatracker.ietf.org/doc/draft-irtf-cfrg-opaque), an asymmetric password-authenticated 11 | key exchange protocol that is secure against pre-computation attacks. It enables a client to authenticate to a server 12 | without ever revealing its password to the server. 13 | 14 | This implementation is developed by one of the authors of the RFC [Internet Draft](https://github.com/cfrg/draft-irtf-cfrg-opaque). 15 | The main branch is in sync with the latest developments of the draft, and [the releases](https://github.com/bytemare/opaque/releases) 16 | correspond to the [official draft versions](https://datatracker.ietf.org/doc/draft-irtf-cfrg-opaque). 17 | 18 | #### What is OPAQUE? 19 | 20 | > OPAQUE is an aPAKE that is secure against pre-computation attacks. OPAQUE provides forward secrecy with 21 | > respect to password leakage while also hiding the password from the server, even during password registration. OPAQUE 22 | > allows applications to increase the difficulty of offline dictionary attacks via iterated hashing or other key 23 | > stretching schemes. OPAQUE is also extensible, allowing clients to safely store and retrieve arbitrary application data 24 | > on servers using only their password. 25 | 26 | #### References 27 | - [The original paper](https://eprint.iacr.org/2018/163.pdf) from Jarecki, Krawczyk, and Xu. 28 | - [OPAQUE is used in WhatsApp](https://www.whatsapp.com/security/WhatsApp_Security_Encrypted_Backups_Whitepaper.pdf) to enable end-to-end encrypted backups. 29 | - [The GitHub repo](https://github.com/cfrg/draft-irtf-cfrg-opaque) where the draft is being specified. 30 | 31 | ## Documentation [![Go Reference](https://pkg.go.dev/badge/github.com/bytemare/opaque.svg)](https://pkg.go.dev/github.com/bytemare/opaque) 32 | 33 | You can find the documentation and usage examples in [the package doc](https://pkg.go.dev/github.com/bytemare/opaque) and [the project wiki](https://github.com/bytemare/opaque/wiki) . 34 | 35 | ## Versioning 36 | 37 | [SemVer](https://semver.org) is used for versioning. For the versions available, see the [tags on the repository](https://github.com/bytemare/opaque/tags). 38 | 39 | Minor v0.x versions match the corresponding CFRG draft version, the master branch implements the latest changes of [the draft development](https://github.com/cfrg/draft-irtf-cfrg-opaque). 40 | 41 | ## Contributing 42 | 43 | Please read [CONTRIBUTING.md](.github/CONTRIBUTING.md) for details on the code of conduct, and the process for submitting pull requests. 44 | 45 | ## License 46 | 47 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 48 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright (C) 2020-2025 Daniel Bourdrez. All Rights Reserved. 4 | // 5 | // This source code is licensed under the MIT license found in the 6 | // LICENSE file in the root directory of this source tree or at 7 | // https://spdx.org/licenses/MIT.html 8 | 9 | package opaque 10 | 11 | import ( 12 | "errors" 13 | "fmt" 14 | 15 | "github.com/bytemare/ecc" 16 | 17 | "github.com/bytemare/opaque/internal" 18 | "github.com/bytemare/opaque/internal/ake" 19 | "github.com/bytemare/opaque/internal/encoding" 20 | "github.com/bytemare/opaque/internal/keyrecovery" 21 | "github.com/bytemare/opaque/internal/masking" 22 | "github.com/bytemare/opaque/internal/oprf" 23 | "github.com/bytemare/opaque/internal/tag" 24 | "github.com/bytemare/opaque/message" 25 | ) 26 | 27 | var ( 28 | // errInvalidMaskedLength happens when unmasking a masked response. 29 | errInvalidMaskedLength = errors.New("invalid masked response length") 30 | 31 | // errKe1Missing happens when GenerateKE3 is called and the client has no Ke1 in state. 32 | errKe1Missing = errors.New("missing KE1 in client state") 33 | ) 34 | 35 | // Client represents an OPAQUE Client, exposing its functions and holding its state. 36 | type Client struct { 37 | Deserialize *Deserializer 38 | OPRF *oprf.Client 39 | Ake *ake.Client 40 | conf *internal.Configuration 41 | } 42 | 43 | // NewClient returns a new Client instantiation given the application Configuration. 44 | func NewClient(c *Configuration) (*Client, error) { 45 | if c == nil { 46 | c = DefaultConfiguration() 47 | } 48 | 49 | conf, err := c.toInternal() 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | return &Client{ 55 | OPRF: conf.OPRF.Client(), 56 | Ake: ake.NewClient(), 57 | Deserialize: &Deserializer{conf: conf}, 58 | conf: conf, 59 | }, nil 60 | } 61 | 62 | // GetConf returns the internal configuration. 63 | func (c *Client) GetConf() *internal.Configuration { 64 | return c.conf 65 | } 66 | 67 | // buildPRK derives the randomized password from the OPRF output. 68 | func (c *Client) buildPRK(evaluation *ecc.Element, ksfSalt, kdfSalt []byte, ksfLength int) []byte { 69 | output := c.OPRF.Finalize(evaluation) 70 | stretched := c.conf.KSF.Harden(output, ksfSalt, ksfLength) 71 | 72 | return c.conf.KDF.Extract(kdfSalt, encoding.Concat(output, stretched)) 73 | } 74 | 75 | // ClientRegistrationInitOptions enables setting internal client values for the client registration. 76 | type ClientRegistrationInitOptions struct { 77 | // OPRFBlind: optional. 78 | OPRFBlind *ecc.Scalar 79 | } 80 | 81 | func getClientRegistrationInitBlind(options []ClientRegistrationInitOptions) *ecc.Scalar { 82 | if len(options) == 0 { 83 | return nil 84 | } 85 | 86 | return options[0].OPRFBlind 87 | } 88 | 89 | // RegistrationInit returns a RegistrationRequest message blinding the given password. 90 | func (c *Client) RegistrationInit( 91 | password []byte, 92 | options ...ClientRegistrationInitOptions, 93 | ) *message.RegistrationRequest { 94 | m := c.OPRF.Blind(password, getClientRegistrationInitBlind(options)) 95 | 96 | return &message.RegistrationRequest{ 97 | BlindedMessage: m, 98 | } 99 | } 100 | 101 | // ClientRegistrationFinalizeOptions enables setting optional client values for the client registration. 102 | type ClientRegistrationFinalizeOptions struct { 103 | // ClientIdentity: optional. 104 | ClientIdentity []byte 105 | // ServerIdentity: optional. 106 | ServerIdentity []byte 107 | // EnvelopeNonce : optional. 108 | EnvelopeNonce []byte 109 | // KDFSalt: optional. 110 | KDFSalt []byte 111 | // KSFSalt: optional. 112 | KSFSalt []byte 113 | // KSFParameters: optional. 114 | KSFParameters []int 115 | // KSFLength: optional. 116 | KSFLength uint32 117 | } 118 | 119 | func (c *Client) initClientRegistrationFinalizeOptions( 120 | options []ClientRegistrationFinalizeOptions, 121 | ) (*keyrecovery.Credentials, []byte, []byte, int) { 122 | if len(options) == 0 { 123 | return &keyrecovery.Credentials{ 124 | ClientIdentity: nil, 125 | ServerIdentity: nil, 126 | EnvelopeNonce: nil, 127 | }, nil, nil, c.conf.Group.ElementLength() 128 | } 129 | 130 | if len(options[0].KSFParameters) != 0 { 131 | c.conf.KSF.Parameterize(options[0].KSFParameters...) 132 | } 133 | 134 | ksfLength := int(options[0].KSFLength) 135 | if ksfLength == 0 { 136 | ksfLength = c.conf.Group.ElementLength() 137 | } 138 | 139 | return &keyrecovery.Credentials{ 140 | ClientIdentity: options[0].ClientIdentity, 141 | ServerIdentity: options[0].ServerIdentity, 142 | EnvelopeNonce: options[0].EnvelopeNonce, 143 | }, options[0].KSFSalt, options[0].KDFSalt, ksfLength 144 | } 145 | 146 | // RegistrationFinalize returns a RegistrationRecord message given the identities and the server's RegistrationResponse. 147 | func (c *Client) RegistrationFinalize( 148 | resp *message.RegistrationResponse, 149 | options ...ClientRegistrationFinalizeOptions, 150 | ) (record *message.RegistrationRecord, exportKey []byte) { 151 | credentials, ksfSalt, kdfSalt, ksfLength := c.initClientRegistrationFinalizeOptions(options) 152 | randomizedPassword := c.buildPRK(resp.EvaluatedMessage, ksfSalt, kdfSalt, ksfLength) 153 | maskingKey := c.conf.KDF.Expand(randomizedPassword, []byte(tag.MaskingKey), c.conf.KDF.Size()) 154 | envelope, clientPublicKey, exportKey := keyrecovery.Store(c.conf, randomizedPassword, resp.Pks, credentials) 155 | 156 | return &message.RegistrationRecord{ 157 | PublicKey: clientPublicKey, 158 | MaskingKey: maskingKey, 159 | Envelope: envelope.Serialize(), 160 | }, exportKey 161 | } 162 | 163 | // GenerateKE1Options enable setting optional values for the session, which default to secure random values if not 164 | // set. 165 | type GenerateKE1Options struct { 166 | // OPRFBlind: optional. 167 | OPRFBlind *ecc.Scalar 168 | // KeyShareSeed: optional. 169 | KeyShareSeed []byte 170 | // AKENonce: optional. 171 | AKENonce []byte 172 | // AKENonceLength: optional, overrides the default length of the nonce to be created if no nonce is provided. 173 | AKENonceLength uint32 174 | } 175 | 176 | func getGenerateKE1Options(options []GenerateKE1Options) (*ecc.Scalar, ake.Options) { 177 | if len(options) != 0 { 178 | return options[0].OPRFBlind, ake.Options{ 179 | KeyShareSeed: options[0].KeyShareSeed, 180 | Nonce: options[0].AKENonce, 181 | NonceLength: options[0].AKENonceLength, 182 | } 183 | } 184 | 185 | return nil, ake.Options{ 186 | KeyShareSeed: nil, 187 | Nonce: nil, 188 | NonceLength: internal.NonceLength, 189 | } 190 | } 191 | 192 | // GenerateKE1 initiates the authentication process, returning a KE1 message blinding the given password. 193 | func (c *Client) GenerateKE1(password []byte, options ...GenerateKE1Options) *message.KE1 { 194 | blind, akeOptions := getGenerateKE1Options(options) 195 | m := c.OPRF.Blind(password, blind) 196 | ke1 := c.Ake.Start(c.conf.Group, akeOptions) 197 | ke1.CredentialRequest = message.NewCredentialRequest(m) 198 | c.Ake.Ke1 = ke1.Serialize() 199 | 200 | return ke1 201 | } 202 | 203 | // GenerateKE3Options enable setting optional client values for the client registration. 204 | type GenerateKE3Options struct { 205 | // ClientIdentity: optional. 206 | ClientIdentity []byte 207 | // ServerIdentity: optional. 208 | ServerIdentity []byte 209 | // KDFSalt: optional. 210 | KDFSalt []byte 211 | // KSFSalt: optional. 212 | KSFSalt []byte 213 | // KSFParameters: optional. 214 | KSFParameters []int 215 | // KSFLength: optional. 216 | KSFLength uint32 217 | } 218 | 219 | func (c *Client) initGenerateKE3Options(options []GenerateKE3Options) (*ake.Identities, []byte, []byte, int) { 220 | if len(options) == 0 { 221 | return &ake.Identities{ 222 | ClientIdentity: nil, 223 | ServerIdentity: nil, 224 | }, nil, nil, c.conf.Group.ElementLength() 225 | } 226 | 227 | if len(options[0].KSFParameters) != 0 { 228 | c.conf.KSF.Parameterize(options[0].KSFParameters...) 229 | } 230 | 231 | ksfLength := int(options[0].KSFLength) 232 | if ksfLength == 0 { 233 | ksfLength = c.conf.Group.ElementLength() 234 | } 235 | 236 | return &ake.Identities{ 237 | ClientIdentity: options[0].ClientIdentity, 238 | ServerIdentity: options[0].ServerIdentity, 239 | }, options[0].KSFSalt, options[0].KDFSalt, ksfLength 240 | } 241 | 242 | // GenerateKE3 returns a KE3 message given the server's KE2 response message and the identities. If the idc 243 | // or ids parameters are nil, the client and server's public keys are taken as identities for both. 244 | func (c *Client) GenerateKE3( 245 | ke2 *message.KE2, options ...GenerateKE3Options, 246 | ) (ke3 *message.KE3, exportKey []byte, err error) { 247 | if len(c.Ake.Ke1) == 0 { 248 | return nil, nil, errKe1Missing 249 | } 250 | 251 | // This test is very important as it avoids buffer overflows in subsequent parsing. 252 | if len(ke2.MaskedResponse) != c.conf.Group.ElementLength()+c.conf.EnvelopeSize { 253 | return nil, nil, errInvalidMaskedLength 254 | } 255 | 256 | identities, ksfSalt, kdfSalt, ksfLength := c.initGenerateKE3Options(options) 257 | 258 | // Finalize the OPRF. 259 | randomizedPassword := c.buildPRK(ke2.EvaluatedMessage, ksfSalt, kdfSalt, ksfLength) 260 | 261 | // Decrypt the masked response. 262 | serverPublicKey, serverPublicKeyBytes, 263 | envelope, err := masking.Unmask(c.conf, randomizedPassword, ke2.MaskingNonce, ke2.MaskedResponse) 264 | if err != nil { 265 | return nil, nil, fmt.Errorf("unmasking: %w", err) 266 | } 267 | 268 | // Recover the client keys. 269 | clientSecretKey, clientPublicKey, 270 | exportKey, err := keyrecovery.Recover( 271 | c.conf, 272 | randomizedPassword, 273 | serverPublicKeyBytes, 274 | identities.ClientIdentity, 275 | identities.ServerIdentity, 276 | envelope) 277 | if err != nil { 278 | return nil, nil, fmt.Errorf("key recovery: %w", err) 279 | } 280 | 281 | // Finalize the AKE. 282 | identities.SetIdentities(clientPublicKey, serverPublicKeyBytes) 283 | 284 | ke3, err = c.Ake.Finalize(c.conf, identities, clientSecretKey, serverPublicKey, ke2) 285 | if err != nil { 286 | return nil, nil, fmt.Errorf("finalizing AKE: %w", err) 287 | } 288 | 289 | return ke3, exportKey, nil 290 | } 291 | 292 | // SessionKey returns the session key if the previous call to GenerateKE3() was successful. 293 | func (c *Client) SessionKey() []byte { 294 | return c.Ake.SessionKey() 295 | } 296 | -------------------------------------------------------------------------------- /deserializer.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright (C) 2020-2025 Daniel Bourdrez. All Rights Reserved. 4 | // 5 | // This source code is licensed under the MIT license found in the 6 | // LICENSE file in the root directory of this source tree or at 7 | // https://spdx.org/licenses/MIT.html 8 | 9 | package opaque 10 | 11 | import ( 12 | "errors" 13 | "fmt" 14 | 15 | "github.com/bytemare/ecc" 16 | 17 | "github.com/bytemare/opaque/internal" 18 | "github.com/bytemare/opaque/message" 19 | ) 20 | 21 | var ( 22 | errInvalidMessageLength = errors.New("invalid message length for the configuration") 23 | errInvalidBlindedData = errors.New("blinded data is an invalid point") 24 | errInvalidClientEPK = errors.New("invalid ephemeral client public key") 25 | errInvalidEvaluatedData = errors.New("invalid OPRF evaluation") 26 | errInvalidServerEPK = errors.New("invalid ephemeral server public key") 27 | errInvalidServerPK = errors.New("invalid server public key") 28 | errInvalidClientPK = errors.New("invalid client public key") 29 | ) 30 | 31 | // Deserializer exposes the message deserialization functions. 32 | type Deserializer struct { 33 | conf *internal.Configuration 34 | } 35 | 36 | // RegistrationRequest takes a serialized RegistrationRequest message and returns a deserialized 37 | // RegistrationRequest structure. 38 | func (d *Deserializer) RegistrationRequest(registrationRequest []byte) (*message.RegistrationRequest, error) { 39 | if len(registrationRequest) != d.conf.OPRF.Group().ElementLength() { 40 | return nil, errInvalidMessageLength 41 | } 42 | 43 | blindedMessage := d.conf.OPRF.Group().NewElement() 44 | if err := blindedMessage.Decode(registrationRequest[:d.conf.OPRF.Group().ElementLength()]); err != nil { 45 | return nil, errInvalidBlindedData 46 | } 47 | 48 | return &message.RegistrationRequest{BlindedMessage: blindedMessage}, nil 49 | } 50 | 51 | func (d *Deserializer) registrationResponseLength() int { 52 | return d.conf.OPRF.Group().ElementLength() + d.conf.Group.ElementLength() 53 | } 54 | 55 | // RegistrationResponse takes a serialized RegistrationResponse message and returns a deserialized 56 | // RegistrationResponse structure. 57 | func (d *Deserializer) RegistrationResponse(registrationResponse []byte) (*message.RegistrationResponse, error) { 58 | if len(registrationResponse) != d.registrationResponseLength() { 59 | return nil, errInvalidMessageLength 60 | } 61 | 62 | evaluatedMessage := d.conf.OPRF.Group().NewElement() 63 | if err := evaluatedMessage.Decode(registrationResponse[:d.conf.OPRF.Group().ElementLength()]); err != nil { 64 | return nil, errInvalidEvaluatedData 65 | } 66 | 67 | pks := d.conf.Group.NewElement() 68 | if err := pks.Decode(registrationResponse[d.conf.OPRF.Group().ElementLength():]); err != nil { 69 | return nil, errInvalidServerPK 70 | } 71 | 72 | return &message.RegistrationResponse{ 73 | EvaluatedMessage: evaluatedMessage, 74 | Pks: pks, 75 | }, nil 76 | } 77 | 78 | func (d *Deserializer) recordLength() int { 79 | return d.conf.Group.ElementLength() + d.conf.Hash.Size() + d.conf.EnvelopeSize 80 | } 81 | 82 | // RegistrationRecord takes a serialized RegistrationRecord message and returns a deserialized 83 | // RegistrationRecord structure. 84 | func (d *Deserializer) RegistrationRecord(record []byte) (*message.RegistrationRecord, error) { 85 | if len(record) != d.recordLength() { 86 | return nil, errInvalidMessageLength 87 | } 88 | 89 | pk := record[:d.conf.Group.ElementLength()] 90 | maskingKey := record[d.conf.Group.ElementLength() : d.conf.Group.ElementLength()+d.conf.Hash.Size()] 91 | env := record[d.conf.Group.ElementLength()+d.conf.Hash.Size():] 92 | 93 | pku := d.conf.Group.NewElement() 94 | if err := pku.Decode(pk); err != nil { 95 | return nil, errInvalidClientPK 96 | } 97 | 98 | return &message.RegistrationRecord{ 99 | PublicKey: pku, 100 | MaskingKey: maskingKey, 101 | Envelope: env, 102 | }, nil 103 | } 104 | 105 | func (d *Deserializer) deserializeCredentialRequest(input []byte) (*message.CredentialRequest, error) { 106 | blindedMessage := d.conf.OPRF.Group().NewElement() 107 | if err := blindedMessage.Decode(input[:d.conf.OPRF.Group().ElementLength()]); err != nil { 108 | return nil, errInvalidBlindedData 109 | } 110 | 111 | return message.NewCredentialRequest(blindedMessage), nil 112 | } 113 | 114 | func (d *Deserializer) deserializeCredentialResponse( 115 | input []byte, 116 | maxResponseLength int, 117 | ) (*message.CredentialResponse, error) { 118 | data := d.conf.OPRF.Group().NewElement() 119 | if err := data.Decode(input[:d.conf.OPRF.Group().ElementLength()]); err != nil { 120 | return nil, errInvalidEvaluatedData 121 | } 122 | 123 | return message.NewCredentialResponse(data, 124 | input[d.conf.OPRF.Group().ElementLength():d.conf.OPRF.Group().ElementLength()+d.conf.NonceLen], 125 | input[d.conf.OPRF.Group().ElementLength()+d.conf.NonceLen:maxResponseLength]), nil 126 | } 127 | 128 | func (d *Deserializer) ke1Length() int { 129 | return d.conf.OPRF.Group().ElementLength() + d.conf.NonceLen + d.conf.Group.ElementLength() 130 | } 131 | 132 | // KE1 takes a serialized KE1 message and returns a deserialized KE1 structure. 133 | func (d *Deserializer) KE1(ke1 []byte) (*message.KE1, error) { 134 | if len(ke1) != d.ke1Length() { 135 | return nil, errInvalidMessageLength 136 | } 137 | 138 | request, err := d.deserializeCredentialRequest(ke1) 139 | if err != nil { 140 | return nil, err 141 | } 142 | 143 | nonceU := ke1[d.conf.OPRF.Group().ElementLength() : d.conf.OPRF.Group().ElementLength()+d.conf.NonceLen] 144 | 145 | epku := d.conf.Group.NewElement() 146 | if err = epku.Decode(ke1[d.conf.OPRF.Group().ElementLength()+d.conf.NonceLen:]); err != nil { 147 | return nil, errInvalidClientEPK 148 | } 149 | 150 | return &message.KE1{ 151 | CredentialRequest: request, 152 | ClientNonce: nonceU, 153 | ClientPublicKeyshare: epku, 154 | }, nil 155 | } 156 | 157 | func (d *Deserializer) ke2LengthWithoutCreds() int { 158 | return d.conf.NonceLen + d.conf.Group.ElementLength() + d.conf.MAC.Size() 159 | } 160 | 161 | func (d *Deserializer) credentialResponseLength() int { 162 | return d.conf.OPRF.Group().ElementLength() + d.conf.NonceLen + d.conf.Group.ElementLength() + d.conf.EnvelopeSize 163 | } 164 | 165 | // KE2 takes a serialized KE2 message and returns a deserialized KE2 structure. 166 | func (d *Deserializer) KE2(ke2 []byte) (*message.KE2, error) { 167 | // size of credential response 168 | maxResponseLength := d.credentialResponseLength() 169 | 170 | // Verify it matches the size of a legal KE2 171 | if len(ke2) != maxResponseLength+d.ke2LengthWithoutCreds() { 172 | return nil, errInvalidMessageLength 173 | } 174 | 175 | cresp, err := d.deserializeCredentialResponse(ke2, maxResponseLength) 176 | if err != nil { 177 | return nil, err 178 | } 179 | 180 | nonceS := ke2[maxResponseLength : maxResponseLength+d.conf.NonceLen] 181 | offset := maxResponseLength + d.conf.NonceLen 182 | epk := ke2[offset : offset+d.conf.Group.ElementLength()] 183 | offset += d.conf.Group.ElementLength() 184 | mac := ke2[offset:] 185 | 186 | epks := d.conf.Group.NewElement() 187 | if err = epks.Decode(epk); err != nil { 188 | return nil, errInvalidServerEPK 189 | } 190 | 191 | return &message.KE2{ 192 | CredentialResponse: cresp, 193 | ServerNonce: nonceS, 194 | ServerPublicKeyshare: epks, 195 | ServerMac: mac, 196 | }, nil 197 | } 198 | 199 | // KE3 takes a serialized KE3 message and returns a deserialized KE3 structure. 200 | func (d *Deserializer) KE3(ke3 []byte) (*message.KE3, error) { 201 | if len(ke3) != d.conf.MAC.Size() { 202 | return nil, errInvalidMessageLength 203 | } 204 | 205 | return &message.KE3{ClientMac: ke3}, nil 206 | } 207 | 208 | // DecodeAkePrivateKey takes a serialized private key (a scalar) and attempts to return it's decoded form. 209 | func (d *Deserializer) DecodeAkePrivateKey(encoded []byte) (*ecc.Scalar, error) { 210 | sk := d.conf.Group.NewScalar() 211 | if err := sk.Decode(encoded); err != nil { 212 | return nil, fmt.Errorf("invalid private key: %w", err) 213 | } 214 | 215 | return sk, nil 216 | } 217 | 218 | // DecodeAkePublicKey takes a serialized public key (a point) and attempts to return it's decoded form. 219 | func (d *Deserializer) DecodeAkePublicKey(encoded []byte) (*ecc.Element, error) { 220 | pk := d.conf.Group.NewElement() 221 | if err := pk.Decode(encoded); err != nil { 222 | return nil, fmt.Errorf("invalid public key: %w", err) 223 | } 224 | 225 | return pk, nil 226 | } 227 | -------------------------------------------------------------------------------- /examples_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright (C) 2020-2025 Daniel Bourdrez. All Rights Reserved. 4 | // 5 | // This source code is licensed under the MIT license found in the 6 | // LICENSE file in the root directory of this source tree or at 7 | // https://spdx.org/licenses/MIT.html 8 | 9 | package opaque_test 10 | 11 | import ( 12 | "bytes" 13 | "crypto" 14 | "encoding/hex" 15 | "fmt" 16 | "log" 17 | "reflect" 18 | 19 | "github.com/bytemare/ksf" 20 | 21 | "github.com/bytemare/opaque" 22 | ) 23 | 24 | var ( 25 | exampleClientRecord *opaque.ClientRecord 26 | secretOprfSeed, serverPrivateKey, serverPublicKey []byte 27 | ) 28 | 29 | func isSameConf(a, b *opaque.Configuration) bool { 30 | if a.OPRF != b.OPRF || 31 | a.KDF != b.KDF || 32 | a.MAC != b.MAC || 33 | a.Hash != b.Hash || 34 | !reflect.DeepEqual(a.KSF, b.KSF) || 35 | a.AKE != b.AKE { 36 | return false 37 | } 38 | 39 | return bytes.Equal(a.Context, b.Context) 40 | } 41 | 42 | // Example_Configuration shows how to instantiate a configuration, which is used to initialize clients and servers from. 43 | // Configurations MUST remain the same for a given client between sessions, or the client won't be able to execute the 44 | // protocol. Configurations can be serialized and deserialized, if you need to save, hardcode, or transmit it. 45 | func Example_configuration() { 46 | // You can compose your own configuration or choose a recommended default configuration. 47 | // The two following configuration setups are the same. 48 | defaultConf := opaque.DefaultConfiguration() 49 | 50 | customConf := &opaque.Configuration{ 51 | OPRF: opaque.RistrettoSha512, 52 | AKE: opaque.RistrettoSha512, 53 | KSF: ksf.Argon2id, 54 | KDF: crypto.SHA512, 55 | MAC: crypto.SHA512, 56 | Hash: crypto.SHA512, 57 | Context: nil, 58 | } 59 | 60 | if !isSameConf(defaultConf, customConf) { 61 | // isSameConf() this is just a demo function to check equality. 62 | log.Fatalln("Oh no! Configurations differ!") 63 | } 64 | 65 | // A configuration can be saved encoded and saved, and later loaded and decoded at runtime. 66 | // Any additional 'Context' is also included. 67 | encoded := defaultConf.Serialize() 68 | fmt.Printf("Encoded Configuration: %s\n", hex.EncodeToString(encoded)) 69 | 70 | // This how you decode that configuration. 71 | conf, err := opaque.DeserializeConfiguration(encoded) 72 | if err != nil { 73 | log.Fatalf("Oh no! Decoding the configurations failed! %v", err) 74 | } 75 | 76 | if !isSameConf(defaultConf, conf) { 77 | log.Fatalln("Oh no! Something went wrong in decoding the configuration!") 78 | } 79 | 80 | fmt.Println("OPAQUE configuration is easy!") 81 | 82 | // Output: Encoded Configuration: 0101010707070000 83 | // OPAQUE configuration is easy! 84 | } 85 | 86 | // Example_ServerSetup shows how to set up the long term values for the OPAQUE server. 87 | // - The secret OPRF seed can be unique for each client or the same for all, but must be 88 | // the same for a given client between registration and all login sessions. 89 | // - The AKE key pair can also be the same for all clients or unique, but must be 90 | // the same for a given client between registration and all login sessions. 91 | func Example_serverSetup() { 92 | // This a straightforward way to use a secure and efficient configuration. 93 | // They have to be run only once in the application's lifecycle, and the output values must be stored appropriately. 94 | serverID := []byte("server-identity") 95 | conf := opaque.DefaultConfiguration() 96 | secretOprfSeed = conf.GenerateOPRFSeed() 97 | serverPrivateKey, serverPublicKey = conf.KeyGen() 98 | 99 | if serverPrivateKey == nil || serverPublicKey == nil || secretOprfSeed == nil { 100 | log.Fatalf("Oh no! Something went wrong setting up the server secrets!") 101 | } 102 | 103 | // Server setup 104 | server, err := conf.Server() 105 | if err != nil { 106 | log.Fatalln(err) 107 | } 108 | 109 | if err := server.SetKeyMaterial(serverID, serverPrivateKey, serverPublicKey, secretOprfSeed); err != nil { 110 | log.Fatalln(err) 111 | } 112 | 113 | fmt.Println("OPAQUE server initialized.") 114 | 115 | // Output: OPAQUE server initialized. 116 | } 117 | 118 | // Example_Deserialization demonstrates a couple of ways to deserialize OPAQUE protocol messages. 119 | // Message interpretation depends on the configuration context it's exchanged in. Hence, we need the corresponding 120 | // configuration. We can then directly deserialize messages from a Configuration or pass them to Client or Server 121 | // instances which can do it as well. 122 | // You must know in advance what message you are expecting, and call the appropriate deserialization function. 123 | func Example_deserialization() { 124 | // Let's say we have this RegistrationRequest message we received on the wire. 125 | registrationMessage, _ := hex.DecodeString("9857e1694af550c515e56a9103292ad07a014b020708d3df57ac4b151f58d323") 126 | 127 | // Pick your configuration. 128 | conf := opaque.DefaultConfiguration() 129 | 130 | // You can directly deserialize and test the message's validity in that configuration by getting a deserializer. 131 | deserializer, err := conf.Deserializer() 132 | if err != nil { 133 | log.Fatalln(err) 134 | } 135 | 136 | requestD, err := deserializer.RegistrationRequest(registrationMessage) 137 | if err != nil { 138 | log.Fatalln(err) 139 | } 140 | 141 | // Or if you already have a Server instance, you can use that also. 142 | server, err := conf.Server() 143 | if err != nil { 144 | log.Fatalln(err) 145 | } 146 | 147 | requestS, err := server.Deserialize.RegistrationRequest(registrationMessage) 148 | if err != nil { 149 | // The error message will tell us what's wrong. 150 | log.Fatalln(err) 151 | } 152 | 153 | // Alternatively, a Client instance can do that as well. 154 | client, err := conf.Client() 155 | if err != nil { 156 | // The error message will tell us what's wrong. 157 | log.Fatalln(err) 158 | } 159 | 160 | requestC, err := client.Deserialize.RegistrationRequest(registrationMessage) 161 | if err != nil { 162 | // The error message will tell us what's wrong. 163 | log.Fatalln(err) 164 | } 165 | 166 | // All these yield the same message. The following is just a test to proof that point. 167 | { 168 | if !reflect.DeepEqual(requestD, requestS) || 169 | !reflect.DeepEqual(requestD, requestC) || 170 | !reflect.DeepEqual(requestS, requestC) { 171 | log.Fatalf("Unexpected divergent RegistrationMessages:\n\t- %v\n\t- %v\n\t- %v", 172 | hex.EncodeToString(requestD.Serialize()), 173 | hex.EncodeToString(requestS.Serialize()), 174 | hex.EncodeToString(requestC.Serialize())) 175 | } 176 | 177 | fmt.Println("OPAQUE messages deserialization is easy!") 178 | } 179 | 180 | // Output: OPAQUE messages deserialization is easy! 181 | } 182 | 183 | // Example_Registration demonstrates in a single function the interactions between a client and a server for the 184 | // registration phase. This is of course a proof-of-concept demonstration, as client and server execute separately. 185 | // The server outputs a ClientRecord and the credential identifier. The latter is a unique identifier for a given 186 | // client (e.g. database entry ID), and that must absolutely stay the same for the whole client existence and 187 | // never be reused. 188 | func Example_registration() { 189 | // The server must have been set up with its long term values once. So we're calling this, here, for the demo. 190 | { 191 | Example_serverSetup() 192 | } 193 | 194 | // Secret client information. 195 | password := []byte("password") 196 | 197 | // Information shared by both client and server. 198 | serverID := []byte("server") 199 | clientID := []byte("username") 200 | conf := opaque.DefaultConfiguration() 201 | 202 | // Runtime instantiation for the client and server. 203 | client, err := conf.Client() 204 | if err != nil { 205 | log.Fatalln(err) 206 | } 207 | 208 | server, err := conf.Server() 209 | if err != nil { 210 | log.Fatalln(err) 211 | } 212 | 213 | // These are the 3 registration messages that will be exchanged. 214 | // The credential identifier credID is a unique identifier for a given client (e.g. database entry ID), and that 215 | // must absolutely stay the same for the whole client existence and never be reused. 216 | var message1, message2, message3 []byte 217 | var credID []byte 218 | 219 | // The client starts, serializes the message, and sends it to the server. 220 | { 221 | c1 := client.RegistrationInit(password) 222 | message1 = c1.Serialize() 223 | } 224 | 225 | // The server receives the encoded message, decodes it, interprets it, and returns its response. 226 | { 227 | request, err := server.Deserialize.RegistrationRequest(message1) 228 | if err != nil { 229 | log.Fatalln(err) 230 | } 231 | 232 | // The server creates a database entry for the client and creates a credential identifier that must absolutely 233 | // be unique among all clients. 234 | credID = opaque.RandomBytes(64) 235 | pks, err := server.Deserialize.DecodeAkePublicKey(serverPublicKey) 236 | if err != nil { 237 | log.Fatalln(err) 238 | } 239 | 240 | // The server uses its public key and secret OPRF seed created at the setup. 241 | response := server.RegistrationResponse(request, pks, credID, secretOprfSeed) 242 | 243 | // The server responds with its serialized response. 244 | message2 = response.Serialize() 245 | } 246 | 247 | // The client deserializes the responses, and sends back its final client record containing the envelope. 248 | { 249 | response, err := client.Deserialize.RegistrationResponse(message2) 250 | if err != nil { 251 | log.Fatalln(err) 252 | } 253 | 254 | // The client produces its record and a client-only-known secret export_key, that the client can use for other purposes (e.g. encrypt 255 | // information to store on the server, and that the server can't decrypt). We don't use in the example here. 256 | record, _ := client.RegistrationFinalize(response, opaque.ClientRegistrationFinalizeOptions{ 257 | ClientIdentity: clientID, 258 | ServerIdentity: serverID, 259 | }) 260 | message3 = record.Serialize() 261 | } 262 | 263 | // Server registers the client record. 264 | { 265 | record, err := server.Deserialize.RegistrationRecord(message3) 266 | if err != nil { 267 | log.Fatalln(err) 268 | } 269 | 270 | exampleClientRecord = &opaque.ClientRecord{ 271 | CredentialIdentifier: credID, 272 | ClientIdentity: clientID, 273 | RegistrationRecord: record, 274 | } 275 | 276 | fmt.Println("OPAQUE registration is easy!") 277 | } 278 | 279 | // Output: OPAQUE server initialized. 280 | // OPAQUE registration is easy! 281 | } 282 | 283 | // Example_LoginKeyExchange demonstrates in a single function the interactions between a client and a server for the 284 | // login phase. 285 | // This is of course a proof-of-concept demonstration, as client and server execute separately. 286 | func Example_loginKeyExchange() { 287 | // For the purpose of this demo, we consider the following registration has already happened. 288 | { 289 | Example_registration() 290 | } 291 | 292 | // Secret client information. 293 | password := []byte("password") 294 | 295 | // Information shared by both client and server. 296 | serverID := []byte("server") 297 | clientID := []byte("username") 298 | conf := opaque.DefaultConfiguration() 299 | 300 | // Runtime instantiation for the client and server. 301 | client, err := conf.Client() 302 | if err != nil { 303 | log.Fatalln(err) 304 | } 305 | 306 | server, err := conf.Server() 307 | if err != nil { 308 | log.Fatalln(err) 309 | } 310 | 311 | if err := server.SetKeyMaterial(serverID, serverPrivateKey, serverPublicKey, secretOprfSeed); err != nil { 312 | log.Fatalln(err) 313 | } 314 | 315 | // These are the 3 login messages that will be exchanged, 316 | // and the respective sessions keys for the client and server. 317 | var message1, message2, message3 []byte 318 | var clientSessionKey, serverSessionKey []byte 319 | 320 | // The client initiates the ball and sends the serialized ke1 to the server. 321 | { 322 | ke1 := client.GenerateKE1(password) 323 | message1 = ke1.Serialize() 324 | } 325 | 326 | // The server interprets ke1, and sends back ke2. 327 | { 328 | ke1, err := server.Deserialize.KE1(message1) 329 | if err != nil { 330 | log.Fatalln(err) 331 | } 332 | 333 | ke2, err := server.GenerateKE2(ke1, exampleClientRecord) 334 | if err != nil { 335 | log.Fatalln(err) 336 | } 337 | 338 | message2 = ke2.Serialize() 339 | } 340 | 341 | // The client interprets ke2. If everything went fine, the server is considered trustworthy and the client 342 | // can use the shared session key and secret export key. 343 | { 344 | ke2, err := client.Deserialize.KE2(message2) 345 | if err != nil { 346 | log.Fatalln(err) 347 | } 348 | 349 | // In this example, we don't use the secret export key. The client sends the serialized ke3 to the server. 350 | ke3, _, err := client.GenerateKE3(ke2, opaque.GenerateKE3Options{ 351 | ClientIdentity: clientID, 352 | ServerIdentity: serverID, 353 | }) 354 | if err != nil { 355 | log.Fatalln(err) 356 | } 357 | 358 | message3 = ke3.Serialize() 359 | 360 | // If no error occurred, the server can be trusted, and the client can use the session key. 361 | clientSessionKey = client.SessionKey() 362 | } 363 | 364 | // The server must absolutely validate this last message to authenticate the client and continue. If this message 365 | // does not return successfully, the server must not send any secret or sensitive information and immediately cease 366 | // the connection. 367 | { 368 | ke3, err := server.Deserialize.KE3(message3) 369 | if err != nil { 370 | log.Fatalln(err) 371 | } 372 | 373 | if err := server.LoginFinish(ke3); err != nil { 374 | log.Fatalln(err) 375 | } 376 | 377 | // If no error occurred at this point, the server can trust the client and safely extract the shared session key. 378 | serverSessionKey = server.SessionKey() 379 | } 380 | 381 | // The following test does not exist in the real world and simply proves the point that the keys match. 382 | if !bytes.Equal(clientSessionKey, serverSessionKey) { 383 | log.Fatalln("Oh no! Abort! The shared session keys don't match!") 384 | } 385 | 386 | fmt.Println("OPAQUE is much awesome!") 387 | // Output: OPAQUE server initialized. 388 | // OPAQUE registration is easy! 389 | // OPAQUE is much awesome! 390 | } 391 | 392 | // Example_FakeResponse shows how to counter some client enumeration attacks by faking an existing client entry. 393 | // Precompute the fake client record, and return it when no valid record was found. 394 | // Use this with the server's GenerateKE1 function whenever a client wants to retrieve an envelope but a client 395 | // entry does not exist. Failing to do so results in an attacker being able to enumerate users. 396 | func Example_fakeResponse() { 397 | // The server must have been set up with its long term values once. So we're calling this, here, for the demo. 398 | { 399 | Example_serverSetup() 400 | } 401 | 402 | // Precompute the fake client record, and return it when no valid record was found. The malicious client will 403 | // purposefully fail, but can't determine the difference with an existing client record. Choose the same 404 | // configuration as in your app. 405 | conf := opaque.DefaultConfiguration() 406 | fakeRecord, err := conf.GetFakeRecord([]byte("fake_client")) 407 | if err != nil { 408 | log.Fatalln(err) 409 | } 410 | 411 | // Later, during protocol execution, let's say this is the fraudulent login message we received, 412 | // for which no client entry exists. 413 | message1, _ := hex.DecodeString("b4d366645e7ae380f9d476e1319e67c1821f7a5d3dfbfc4e26c7898351979139" + 414 | "0ea528fc609b4393b0353e85fdbb20c6067c11919f40d93d8bb229967fc2878c" + 415 | "209786ef4b960bfbfe10481c1fd301300fc72dc4234a1e829b556c720f904d30") 416 | 417 | // Continue as usual, using the fake record in lieu of the (non-)existing one. The server the sends 418 | // back the serialized ke2 message message2. 419 | var message2 []byte 420 | { 421 | serverID := []byte("server") 422 | server, err := conf.Server() 423 | if err != nil { 424 | log.Fatalln(err) 425 | } 426 | 427 | if err := server.SetKeyMaterial(serverID, serverPrivateKey, serverPublicKey, secretOprfSeed); err != nil { 428 | log.Fatalln(err) 429 | } 430 | 431 | ke1, err := server.Deserialize.KE1(message1) 432 | if err != nil { 433 | log.Fatalln(err) 434 | } 435 | 436 | ke2, err := server.GenerateKE2(ke1, fakeRecord) 437 | if err != nil { 438 | log.Fatalln(err) 439 | } 440 | 441 | message2 = ke2.Serialize() 442 | } 443 | 444 | // The following is just a test to check everything went fine. 445 | { 446 | if len(message2) == 0 { 447 | log.Fatalln("Fake KE2 is unexpectedly empty.") 448 | } 449 | 450 | fmt.Println("Thwarting OPAQUE client enumeration is easy!") 451 | } 452 | 453 | // Output: OPAQUE server initialized. 454 | // Thwarting OPAQUE client enumeration is easy! 455 | } 456 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/bytemare/opaque 2 | 3 | go 1.24.2 4 | 5 | require ( 6 | github.com/bytemare/ecc v0.9.0 7 | github.com/bytemare/hash v0.5.2 8 | github.com/bytemare/ksf v0.3.0 9 | ) 10 | 11 | require ( 12 | filippo.io/edwards25519 v1.1.0 // indirect 13 | filippo.io/nistec v0.0.3 // indirect 14 | github.com/bytemare/hash2curve v0.5.4 // indirect 15 | github.com/bytemare/secp256k1 v0.3.0 // indirect 16 | github.com/gtank/ristretto255 v0.1.2 // indirect 17 | golang.org/x/crypto v0.38.0 // indirect 18 | golang.org/x/sys v0.33.0 // indirect 19 | ) 20 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= 2 | filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 3 | filippo.io/nistec v0.0.3 h1:h336Je2jRDZdBCLy2fLDUd9E2unG32JLwcJi0JQE9Cw= 4 | filippo.io/nistec v0.0.3/go.mod h1:84fxC9mi+MhC2AERXI4LSa8cmSVOzrFikg6hZ4IfCyw= 5 | github.com/bytemare/ecc v0.9.0 h1:6+gqn9l63bd82nQVwIpW3LFtsnIsy8V4mvlybAYq6II= 6 | github.com/bytemare/ecc v0.9.0/go.mod h1:3sdNyM1oi4YwcUkxlogZT/CnahwrH38T7a1wc8JsICQ= 7 | github.com/bytemare/hash v0.5.2 h1:quCTPpPMh+Elg1autId1hQhI6QyVKj3d9Prn0l2X8oY= 8 | github.com/bytemare/hash v0.5.2/go.mod h1:SfyIe/M9RA+p48z/kYLMJCMVYepaLLkvxrnnqUaY6Yw= 9 | github.com/bytemare/hash2curve v0.5.4 h1:WLgEKcoZKHZ41a/TApFMxhaLYHNh9lL2DNAIZCVCLuM= 10 | github.com/bytemare/hash2curve v0.5.4/go.mod h1:XxXWaFy2g3AsIPovVp4Q8g9GDHNfr2NStnYNC+z1WRo= 11 | github.com/bytemare/ksf v0.3.0 h1:5gxry79Ql6TcBfgKFJqFQA3GmTbWP7660zwvb5NaNek= 12 | github.com/bytemare/ksf v0.3.0/go.mod h1:QJDwBqGhcd1G5mx+Hir5a0pxRIzRdAXUbR9dSDp6dTU= 13 | github.com/bytemare/secp256k1 v0.3.0 h1:Te2HjAtTRKbqRDBOIMKDFkILTqd6YZ+sIgXvrJ7BaB0= 14 | github.com/bytemare/secp256k1 v0.3.0/go.mod h1:Z2HjKm36tC+Qz51fe1eqMsAM99oTTNlvSuPmPyVXGm0= 15 | github.com/gtank/ristretto255 v0.1.2 h1:JEqUCPA1NvLq5DwYtuzigd7ss8fwbYay9fi4/5uMzcc= 16 | github.com/gtank/ristretto255 v0.1.2/go.mod h1:Ph5OpO6c7xKUGROZfWVLiJf9icMDwUeIvY4OmlYW69o= 17 | golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= 18 | golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= 19 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 20 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 21 | -------------------------------------------------------------------------------- /internal/ake/3dh.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright (C) 2020-2025 Daniel Bourdrez. All Rights Reserved. 4 | // 5 | // This source code is licensed under the MIT license found in the 6 | // LICENSE file in the root directory of this source tree or at 7 | // https://spdx.org/licenses/MIT.html 8 | 9 | // Package ake provides high-level functions for the 3DH AKE. 10 | package ake 11 | 12 | import ( 13 | "github.com/bytemare/ecc" 14 | 15 | "github.com/bytemare/opaque/internal" 16 | "github.com/bytemare/opaque/internal/encoding" 17 | "github.com/bytemare/opaque/internal/oprf" 18 | "github.com/bytemare/opaque/internal/tag" 19 | "github.com/bytemare/opaque/message" 20 | ) 21 | 22 | // KeyGen returns private and public keys in the ecc. 23 | func KeyGen(id ecc.Group) (privateKey, publicKey []byte) { 24 | scalar := id.NewScalar().Random() 25 | point := id.Base().Multiply(scalar) 26 | 27 | return scalar.Encode(), point.Encode() 28 | } 29 | 30 | func diffieHellman(s *ecc.Scalar, e *ecc.Element) *ecc.Element { 31 | /* 32 | if id == ecc.Ristretto255Sha512 || id == ecc.P256Sha256 { 33 | e.Copy().Multiply(s) 34 | } 35 | 36 | if id == ecc.Curve25519 { 37 | // TODO 38 | } 39 | */ 40 | return e.Copy().Multiply(s) 41 | } 42 | 43 | // Identities holds the client and server identities. 44 | type Identities struct { 45 | ClientIdentity []byte 46 | ServerIdentity []byte 47 | } 48 | 49 | // SetIdentities sets the client and server identities to their respective public key if not set. 50 | func (id *Identities) SetIdentities(clientPublicKey *ecc.Element, serverPublicKey []byte) *Identities { 51 | if id.ClientIdentity == nil { 52 | id.ClientIdentity = clientPublicKey.Encode() 53 | } 54 | 55 | if id.ServerIdentity == nil { 56 | id.ServerIdentity = serverPublicKey 57 | } 58 | 59 | return id 60 | } 61 | 62 | // Options enable setting optional ephemeral values, which default to secure random values if not set. 63 | type Options struct { 64 | // KeyShareSeed: optional. 65 | KeyShareSeed []byte 66 | // Nonce: optional. 67 | Nonce []byte 68 | // NonceLength: optional, overrides the default length of the nonce to be created if no nonce is provided. 69 | NonceLength uint32 70 | } 71 | 72 | func (o *Options) init() { 73 | if o.KeyShareSeed == nil { 74 | o.KeyShareSeed = internal.RandomBytes(internal.SeedLength) 75 | } 76 | 77 | if o.NonceLength == 0 { 78 | o.NonceLength = internal.NonceLength 79 | } 80 | 81 | if len(o.Nonce) == 0 { 82 | o.Nonce = internal.RandomBytes(int(o.NonceLength)) 83 | } 84 | } 85 | 86 | type values struct { 87 | ephemeralSecretKey *ecc.Scalar 88 | nonce []byte 89 | } 90 | 91 | // GetEphemeralSecretKey returns the state's ephemeral secret key. 92 | func (v *values) GetEphemeralSecretKey() *ecc.Scalar { 93 | return v.ephemeralSecretKey 94 | } 95 | 96 | // GetNonce returns the secret nonce. 97 | func (v *values) GetNonce() []byte { 98 | return v.nonce 99 | } 100 | 101 | func (v *values) flush() { 102 | if v.ephemeralSecretKey != nil { 103 | v.ephemeralSecretKey.Zero() 104 | v.ephemeralSecretKey = nil 105 | } 106 | 107 | v.nonce = nil 108 | } 109 | 110 | // setOptions sets optional values. 111 | // There's no effect if ephemeralSecretKey and nonce have already been set in a previous call. 112 | func (v *values) setOptions(g ecc.Group, options Options) *ecc.Element { 113 | options.init() 114 | 115 | if v.ephemeralSecretKey == nil { 116 | v.ephemeralSecretKey = oprf.IDFromGroup(g). 117 | DeriveKey(options.KeyShareSeed, []byte(tag.DeriveDiffieHellmanKeyPair)) 118 | } 119 | 120 | if v.nonce == nil { 121 | v.nonce = options.Nonce 122 | } 123 | 124 | return g.Base().Multiply(v.ephemeralSecretKey) 125 | } 126 | 127 | func k3dh( 128 | p1 *ecc.Element, 129 | s1 *ecc.Scalar, 130 | p2 *ecc.Element, 131 | s2 *ecc.Scalar, 132 | p3 *ecc.Element, 133 | s3 *ecc.Scalar, 134 | ) []byte { 135 | e1 := diffieHellman(s1, p1).Encode() 136 | e2 := diffieHellman(s2, p2).Encode() 137 | e3 := diffieHellman(s3, p3).Encode() 138 | 139 | return encoding.Concat3(e1, e2, e3) 140 | } 141 | 142 | func core3DH( 143 | conf *internal.Configuration, identities *Identities, ikm, ke1 []byte, ke2 *message.KE2, 144 | ) (sessionSecret, macS, macC []byte) { 145 | initTranscript(conf, identities, ke1, ke2) 146 | 147 | serverMacKey, clientMacKey, sessionSecret := deriveKeys(conf.KDF, ikm, conf.Hash.Sum()) // preamble 148 | serverMac := conf.MAC.MAC(serverMacKey, conf.Hash.Sum()) // transcript2 149 | conf.Hash.Write(serverMac) 150 | transcript3 := conf.Hash.Sum() 151 | clientMac := conf.MAC.MAC(clientMacKey, transcript3) 152 | 153 | return sessionSecret, serverMac, clientMac 154 | } 155 | 156 | func buildLabel(length int, label, context []byte) []byte { 157 | return encoding.Concat3( 158 | encoding.I2OSP(length, 2), 159 | encoding.EncodeVectorLen(append([]byte(tag.LabelPrefix), label...), 1), 160 | encoding.EncodeVectorLen(context, 1)) 161 | } 162 | 163 | func expand(h *internal.KDF, secret, hkdfLabel []byte) []byte { 164 | return h.Expand(secret, hkdfLabel, h.Size()) 165 | } 166 | 167 | func expandLabel(h *internal.KDF, secret, label, context []byte) []byte { 168 | hkdfLabel := buildLabel(h.Size(), label, context) 169 | return expand(h, secret, hkdfLabel) 170 | } 171 | 172 | func deriveSecret(h *internal.KDF, secret, label, context []byte) []byte { 173 | return expandLabel(h, secret, label, context) 174 | } 175 | 176 | func initTranscript(conf *internal.Configuration, identities *Identities, ke1 []byte, ke2 *message.KE2) { 177 | encodedClientID := encoding.EncodeVector(identities.ClientIdentity) 178 | encodedServerID := encoding.EncodeVector(identities.ServerIdentity) 179 | conf.Hash.Write(encoding.Concatenate([]byte(tag.VersionTag), encoding.EncodeVector(conf.Context), 180 | encodedClientID, ke1, 181 | encodedServerID, ke2.CredentialResponse.Serialize(), ke2.ServerNonce, ke2.ServerPublicKeyshare.Encode())) 182 | } 183 | 184 | func deriveKeys(h *internal.KDF, ikm, context []byte) (serverMacKey, clientMacKey, sessionSecret []byte) { 185 | prk := h.Extract(nil, ikm) 186 | handshakeSecret := deriveSecret(h, prk, []byte(tag.Handshake), context) 187 | sessionSecret = deriveSecret(h, prk, []byte(tag.SessionKey), context) 188 | serverMacKey = expandLabel(h, handshakeSecret, []byte(tag.MacServer), nil) 189 | clientMacKey = expandLabel(h, handshakeSecret, []byte(tag.MacClient), nil) 190 | 191 | return serverMacKey, clientMacKey, sessionSecret 192 | } 193 | -------------------------------------------------------------------------------- /internal/ake/client.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright (C) 2020-2025 Daniel Bourdrez. All Rights Reserved. 4 | // 5 | // This source code is licensed under the MIT license found in the 6 | // LICENSE file in the root directory of this source tree or at 7 | // https://spdx.org/licenses/MIT.html 8 | 9 | package ake 10 | 11 | import ( 12 | "errors" 13 | 14 | "github.com/bytemare/ecc" 15 | 16 | "github.com/bytemare/opaque/internal" 17 | "github.com/bytemare/opaque/message" 18 | ) 19 | 20 | var errAkeInvalidServerMac = errors.New("invalid server mac") 21 | 22 | // Client exposes the client's AKE functions and holds its state. 23 | type Client struct { 24 | values 25 | Ke1 []byte 26 | sessionSecret []byte 27 | } 28 | 29 | // NewClient returns a new, empty, 3DH client. 30 | func NewClient() *Client { 31 | return &Client{ 32 | values: values{ 33 | ephemeralSecretKey: nil, 34 | nonce: nil, 35 | }, 36 | Ke1: nil, 37 | sessionSecret: nil, 38 | } 39 | } 40 | 41 | // Start initiates the 3DH protocol, and returns a KE1 message with clientInfo. 42 | func (c *Client) Start(cs ecc.Group, options Options) *message.KE1 { 43 | epk := c.setOptions(cs, options) 44 | 45 | return &message.KE1{ 46 | CredentialRequest: nil, 47 | ClientNonce: c.nonce, 48 | ClientPublicKeyshare: epk, 49 | } 50 | } 51 | 52 | // Finalize verifies and responds to KE3. If the handshake is successful, the session key is stored and this functions 53 | // returns a KE3 message. 54 | func (c *Client) Finalize( 55 | conf *internal.Configuration, 56 | identities *Identities, 57 | clientSecretKey *ecc.Scalar, 58 | serverPublicKey *ecc.Element, 59 | ke2 *message.KE2, 60 | ) (*message.KE3, error) { 61 | ikm := k3dh( 62 | ke2.ServerPublicKeyshare, 63 | c.ephemeralSecretKey, 64 | serverPublicKey, 65 | c.ephemeralSecretKey, 66 | ke2.ServerPublicKeyshare, 67 | clientSecretKey, 68 | ) 69 | sessionSecret, serverMac, clientMac := core3DH(conf, identities, ikm, c.Ke1, ke2) 70 | 71 | if !conf.MAC.Equal(serverMac, ke2.ServerMac) { 72 | return nil, errAkeInvalidServerMac 73 | } 74 | 75 | c.sessionSecret = sessionSecret 76 | 77 | return &message.KE3{ClientMac: clientMac}, nil 78 | } 79 | 80 | // SessionKey returns the secret shared session key if a previous call to Finalize() was successful. 81 | func (c *Client) SessionKey() []byte { 82 | return c.sessionSecret 83 | } 84 | 85 | // Flush sets all the client's session related internal AKE values to nil. 86 | func (c *Client) Flush() { 87 | c.flush() 88 | c.sessionSecret = nil 89 | } 90 | -------------------------------------------------------------------------------- /internal/ake/server.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright (C) 2020-2025 Daniel Bourdrez. All Rights Reserved. 4 | // 5 | // This source code is licensed under the MIT license found in the 6 | // LICENSE file in the root directory of this source tree or at 7 | // https://spdx.org/licenses/MIT.html 8 | 9 | package ake 10 | 11 | import ( 12 | "errors" 13 | 14 | "github.com/bytemare/ecc" 15 | 16 | "github.com/bytemare/opaque/internal" 17 | "github.com/bytemare/opaque/message" 18 | ) 19 | 20 | var errStateNotEmpty = errors.New("existing state is not empty") 21 | 22 | // Server exposes the server's AKE functions and holds its state. 23 | type Server struct { 24 | values 25 | clientMac []byte 26 | sessionSecret []byte 27 | } 28 | 29 | // NewServer returns a new, empty, 3DH server. 30 | func NewServer() *Server { 31 | return &Server{ 32 | values: values{ 33 | ephemeralSecretKey: nil, 34 | nonce: nil, 35 | }, 36 | clientMac: nil, 37 | sessionSecret: nil, 38 | } 39 | } 40 | 41 | // Response produces a 3DH server response message. 42 | func (s *Server) Response( 43 | conf *internal.Configuration, 44 | identities *Identities, 45 | serverSecretKey *ecc.Scalar, 46 | clientPublicKey *ecc.Element, 47 | ke1 *message.KE1, 48 | response *message.CredentialResponse, 49 | options Options, 50 | ) *message.KE2 { 51 | epks := s.setOptions(conf.Group, options) 52 | 53 | ke2 := &message.KE2{ 54 | CredentialResponse: response, 55 | ServerNonce: s.nonce, 56 | ServerPublicKeyshare: epks, 57 | ServerMac: nil, 58 | } 59 | 60 | ikm := k3dh( 61 | ke1.ClientPublicKeyshare, 62 | s.ephemeralSecretKey, 63 | ke1.ClientPublicKeyshare, 64 | serverSecretKey, 65 | clientPublicKey, 66 | s.ephemeralSecretKey, 67 | ) 68 | sessionSecret, serverMac, clientMac := core3DH(conf, identities, ikm, ke1.Serialize(), ke2) 69 | s.sessionSecret = sessionSecret 70 | s.clientMac = clientMac 71 | ke2.ServerMac = serverMac 72 | 73 | return ke2 74 | } 75 | 76 | // Finalize verifies the authentication tag contained in ke3. 77 | func (s *Server) Finalize(conf *internal.Configuration, ke3 *message.KE3) bool { 78 | return conf.MAC.Equal(s.clientMac, ke3.ClientMac) 79 | } 80 | 81 | // SessionKey returns the secret shared session key if a previous call to Response() was successful. 82 | func (s *Server) SessionKey() []byte { 83 | return s.sessionSecret 84 | } 85 | 86 | // ExpectedMAC returns the expected client MAC if a previous call to Response() was successful. 87 | func (s *Server) ExpectedMAC() []byte { 88 | return s.clientMac 89 | } 90 | 91 | // SerializeState will return a []byte containing internal state of the Server. 92 | func (s *Server) SerializeState() []byte { 93 | state := make([]byte, len(s.clientMac)+len(s.sessionSecret)) 94 | 95 | i := copy(state, s.clientMac) 96 | copy(state[i:], s.sessionSecret) 97 | 98 | return state 99 | } 100 | 101 | // SetState will set the given clientMac and sessionSecret in the server's internal state. 102 | func (s *Server) SetState(clientMac, sessionSecret []byte) error { 103 | if len(s.clientMac) != 0 || len(s.sessionSecret) != 0 { 104 | return errStateNotEmpty 105 | } 106 | 107 | s.clientMac = clientMac 108 | s.sessionSecret = sessionSecret 109 | 110 | return nil 111 | } 112 | 113 | // Flush sets all the server's session related internal AKE values to nil. 114 | func (s *Server) Flush() { 115 | s.flush() 116 | s.clientMac = nil 117 | s.sessionSecret = nil 118 | } 119 | -------------------------------------------------------------------------------- /internal/configuration.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright (C) 2020-2025 Daniel Bourdrez. All Rights Reserved. 4 | // 5 | // This source code is licensed under the MIT license found in the 6 | // LICENSE file in the root directory of this source tree or at 7 | // https://spdx.org/licenses/MIT.html 8 | 9 | // Package internal provides values, structures, and functions to operate OPAQUE that are not part of the public API. 10 | package internal 11 | 12 | import ( 13 | "crypto/rand" 14 | "errors" 15 | "fmt" 16 | 17 | "github.com/bytemare/ecc" 18 | 19 | "github.com/bytemare/opaque/internal/oprf" 20 | ) 21 | 22 | const ( 23 | // NonceLength is the default length used for nonces. 24 | NonceLength = 32 25 | 26 | // SeedLength is the default length used for seeds. 27 | SeedLength = oprf.SeedLength 28 | ) 29 | 30 | // ErrConfigurationInvalidLength happens when deserializing a configuration of invalid length. 31 | var ErrConfigurationInvalidLength = errors.New("invalid encoded configuration length") 32 | 33 | // Configuration is the internal representation of the instance runtime parameters. 34 | type Configuration struct { 35 | KDF *KDF 36 | MAC *Mac 37 | Hash *Hash 38 | KSF *KSF 39 | OPRF oprf.Identifier 40 | Context []byte 41 | NonceLen int 42 | EnvelopeSize int 43 | Group ecc.Group 44 | } 45 | 46 | // RandomBytes returns random bytes of length len (wrapper for crypto/rand). 47 | func RandomBytes(length int) []byte { 48 | r := make([]byte, length) 49 | if _, err := rand.Read(r); err != nil { 50 | // We can as well not panic and try again in a loop and a counter to stop. 51 | panic(fmt.Errorf("unexpected error in generating random bytes : %w", err)) 52 | } 53 | 54 | return r 55 | } 56 | -------------------------------------------------------------------------------- /internal/encoding/encoding.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright (C) 2020-2025 Daniel Bourdrez. All Rights Reserved. 4 | // 5 | // This source code is licensed under the MIT license found in the 6 | // LICENSE file in the root directory of this source tree or at 7 | // https://spdx.org/licenses/MIT.html 8 | 9 | // Package encoding provides encoding utilities. 10 | package encoding 11 | 12 | import "errors" 13 | 14 | var ( 15 | errHeaderLength = errors.New("insufficient header length for decoding") 16 | errTotalLength = errors.New("insufficient total length for decoding") 17 | ) 18 | 19 | // EncodeVectorLen returns the input prepended with a byte encoding of its length. 20 | func EncodeVectorLen(input []byte, length uint16) []byte { 21 | return append(I2OSP(len(input), length), input...) 22 | } 23 | 24 | // EncodeVector returns the input with a two-byte encoding of its length. 25 | func EncodeVector(input []byte) []byte { 26 | return EncodeVectorLen(input, 2) 27 | } 28 | 29 | func decodeVectorLen(in []byte, size int) (data []byte, offset int, err error) { 30 | if len(in) < size { 31 | return nil, 0, errHeaderLength 32 | } 33 | 34 | dataLen := OS2IP(in[0:size]) 35 | offset = size + dataLen 36 | 37 | if len(in) < offset { 38 | return nil, 0, errTotalLength 39 | } 40 | 41 | return in[size:offset], offset, nil 42 | } 43 | 44 | // DecodeVector returns the byte-slice of length indexed in the first two bytes. 45 | func DecodeVector(in []byte) (data []byte, offset int, err error) { 46 | return decodeVectorLen(in, 2) 47 | } 48 | -------------------------------------------------------------------------------- /internal/encoding/i2osp.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright (C) 2020-2025 Daniel Bourdrez. All Rights Reserved. 4 | // 5 | // This source code is licensed under the MIT license found in the 6 | // LICENSE file in the root directory of this source tree or at 7 | // https://spdx.org/licenses/MIT.html 8 | 9 | package encoding 10 | 11 | import ( 12 | "encoding/binary" 13 | "errors" 14 | ) 15 | 16 | var ( 17 | errInputNegative = errors.New("negative input") 18 | errInputLarge = errors.New("input is too high for length") 19 | errLengthNegative = errors.New("length is negative or 0") 20 | errLengthTooBig = errors.New("requested length is > 4") 21 | 22 | errInputEmpty = errors.New("nil or empty input") 23 | errInputTooLarge = errors.New("input too large for integer") 24 | ) 25 | 26 | // I2OSP 32-bit Integer to Octet Stream Primitive on maximum 4 bytes. 27 | func I2OSP(value int, length uint16) []byte { 28 | if length <= 0 { 29 | panic(errLengthNegative) 30 | } 31 | 32 | if length > 4 { 33 | panic(errLengthTooBig) 34 | } 35 | 36 | out := make([]byte, 4) 37 | 38 | switch v := value; { 39 | case v < 0: 40 | panic(errInputNegative) 41 | case v >= 1<<(8*length): 42 | panic(errInputLarge) 43 | case length == 1: 44 | binary.BigEndian.PutUint16(out, uint16(v)) //nolint:gosec // overflow is checked beforehand. 45 | return out[1:2] 46 | case length == 2: 47 | binary.BigEndian.PutUint16(out, uint16(v)) //nolint:gosec // overflow is checked beforehand. 48 | return out[:2] 49 | case length == 3: 50 | binary.BigEndian.PutUint32(out, uint32(v)) //nolint:gosec // overflow is checked beforehand. 51 | return out[1:] 52 | default: // length == 4 53 | binary.BigEndian.PutUint32(out, uint32(v)) //nolint:gosec // overflow is checked beforehand. 54 | return out 55 | } 56 | } 57 | 58 | // OS2IP Octet Stream to Integer Primitive on maximum 4 bytes / 32 bits. 59 | func OS2IP(input []byte) int { 60 | switch len(input) { 61 | case 0: 62 | panic(errInputEmpty) 63 | case 1: 64 | b := []byte{0, input[0]} 65 | return int(binary.BigEndian.Uint16(b)) 66 | case 2: 67 | return int(binary.BigEndian.Uint16(input)) 68 | case 3: 69 | b := append([]byte{0}, input...) 70 | return int(binary.BigEndian.Uint32(b)) 71 | case 4: 72 | return int(binary.BigEndian.Uint32(input)) 73 | default: 74 | panic(errInputTooLarge) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /internal/encoding/misc.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright (C) 2020-2025 Daniel Bourdrez. All Rights Reserved. 4 | // 5 | // This source code is licensed under the MIT license found in the 6 | // LICENSE file in the root directory of this source tree or at 7 | // https://spdx.org/licenses/MIT.html 8 | 9 | package encoding 10 | 11 | // Concat returns the concatenation of the two input byte strings. 12 | func Concat(a, b []byte) []byte { 13 | e := make([]byte, 0, len(a)+len(b)) 14 | e = append(e, a...) 15 | e = append(e, b...) 16 | 17 | return e 18 | } 19 | 20 | // Concat3 returns the concatenation of the three input byte strings. 21 | func Concat3(a, b, c []byte) []byte { 22 | e := make([]byte, 0, len(a)+len(b)+len(c)) 23 | e = append(e, a...) 24 | e = append(e, b...) 25 | e = append(e, c...) 26 | 27 | return e 28 | } 29 | 30 | // SuffixString returns the concatenation of the input byte string and the string argument. 31 | func SuffixString(a []byte, b string) []byte { 32 | e := make([]byte, 0, len(a)+len(b)) 33 | e = append(e, a...) 34 | e = append(e, b...) 35 | 36 | return e 37 | } 38 | 39 | // Concatenate takes the variadic array of input and returns a concatenation of it. 40 | func Concatenate(input ...[]byte) []byte { 41 | length := 0 42 | for _, b := range input { 43 | length += len(b) 44 | } 45 | 46 | buf := make([]byte, 0, length) 47 | 48 | for _, in := range input { 49 | buf = append(buf, in...) 50 | } 51 | 52 | return buf 53 | } 54 | -------------------------------------------------------------------------------- /internal/hash.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright (C) 2020-2025 Daniel Bourdrez. All Rights Reserved. 4 | // 5 | // This source code is licensed under the MIT license found in the 6 | // LICENSE file in the root directory of this source tree or at 7 | // https://spdx.org/licenses/MIT.html 8 | 9 | package internal 10 | 11 | import ( 12 | "crypto" 13 | "crypto/hmac" 14 | 15 | "github.com/bytemare/hash" 16 | "github.com/bytemare/ksf" 17 | ) 18 | 19 | // NewKDF returns a newly instantiated KDF. 20 | func NewKDF(id crypto.Hash) *KDF { 21 | return &KDF{h: hash.FromCrypto(id).GetHashFunction()} 22 | } 23 | 24 | // KDF wraps a hash function and exposes KDF methods. 25 | type KDF struct { 26 | h *hash.Fixed 27 | } 28 | 29 | // Extract exposes an Extract only KDF method. 30 | func (k *KDF) Extract(salt, ikm []byte) []byte { 31 | return k.h.HKDFExtract(ikm, salt) 32 | } 33 | 34 | // Expand exposes an Expand only KDF method. 35 | func (k *KDF) Expand(key, info []byte, length int) []byte { 36 | return k.h.HKDFExpand(key, info, length) 37 | } 38 | 39 | // Size returns the output size of the Extract method. 40 | func (k *KDF) Size() int { 41 | return k.h.Size() 42 | } 43 | 44 | // NewMac returns a newly instantiated Mac. 45 | func NewMac(id crypto.Hash) *Mac { 46 | return &Mac{h: hash.FromCrypto(id).GetHashFunction()} 47 | } 48 | 49 | // Mac wraps a hash function and exposes Message Authentication Code methods. 50 | type Mac struct { 51 | h *hash.Fixed 52 | } 53 | 54 | // Equal returns a constant-time comparison of the input. 55 | func (m *Mac) Equal(a, b []byte) bool { 56 | return hmac.Equal(a, b) 57 | } 58 | 59 | // MAC computes a MAC over the message using key. 60 | func (m *Mac) MAC(key, message []byte) []byte { 61 | return m.h.Hmac(message, key) 62 | } 63 | 64 | // Size returns the MAC's output length. 65 | func (m *Mac) Size() int { 66 | return m.h.Size() 67 | } 68 | 69 | // NewHash returns a newly instantiated Hash. 70 | func NewHash(id crypto.Hash) *Hash { 71 | return &Hash{h: hash.FromCrypto(id).GetHashFunction()} 72 | } 73 | 74 | // Hash wraps a hash function and exposes only necessary hashing methods. 75 | type Hash struct { 76 | h *hash.Fixed 77 | } 78 | 79 | // Size returns the output size of the hashing function. 80 | func (h *Hash) Size() int { 81 | return h.h.Size() 82 | } 83 | 84 | // Sum returns the current hash of the running state. 85 | func (h *Hash) Sum() []byte { 86 | return h.h.Sum(nil) 87 | } 88 | 89 | // Write adds input to the running state. 90 | func (h *Hash) Write(p []byte) { 91 | _, _ = h.h.Write(p) 92 | } 93 | 94 | // NewKSF returns a newly instantiated KSF. 95 | func NewKSF(id ksf.Identifier) *KSF { 96 | if id == 0 { 97 | return &KSF{&IdentityKSF{}} 98 | } 99 | 100 | return &KSF{id.Get()} 101 | } 102 | 103 | // KSF wraps a key stretching function and exposes its functions. 104 | type KSF struct { 105 | ksfInterface 106 | } 107 | 108 | type ksfInterface interface { 109 | // Harden uses default parameters for the key derivation function over the input password and salt. 110 | Harden(password, salt []byte, length int) []byte 111 | 112 | // Parameterize replaces the functions parameters with the new ones. 113 | // Must match the amount of parameters for the KSF. 114 | Parameterize(parameters ...int) 115 | } 116 | 117 | // IdentityKSF represents a KSF with no operations. 118 | type IdentityKSF struct{} 119 | 120 | // Harden returns the password as is. 121 | func (i IdentityKSF) Harden(password, _ []byte, _ int) []byte { 122 | return password 123 | } 124 | 125 | // Parameterize applies KSF parameters if defined. 126 | func (i IdentityKSF) Parameterize(_ ...int) { 127 | // no-op 128 | } 129 | -------------------------------------------------------------------------------- /internal/keyrecovery/envelope.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright (C) 2020-2025 Daniel Bourdrez. All Rights Reserved. 4 | // 5 | // This source code is licensed under the MIT license found in the 6 | // LICENSE file in the root directory of this source tree or at 7 | // https://spdx.org/licenses/MIT.html 8 | 9 | // Package keyrecovery provides utility functions and structures allowing credential management. 10 | package keyrecovery 11 | 12 | import ( 13 | "errors" 14 | 15 | "github.com/bytemare/ecc" 16 | 17 | "github.com/bytemare/opaque/internal" 18 | "github.com/bytemare/opaque/internal/encoding" 19 | "github.com/bytemare/opaque/internal/oprf" 20 | "github.com/bytemare/opaque/internal/tag" 21 | ) 22 | 23 | var errEnvelopeInvalidMac = errors.New("invalid envelope authentication tag") 24 | 25 | // Credentials structure is currently used for testing purposes. 26 | type Credentials struct { 27 | ClientIdentity, ServerIdentity []byte 28 | EnvelopeNonce []byte // testing: integrated to support testing 29 | } 30 | 31 | // Envelope represents the OPAQUE envelope. 32 | type Envelope struct { 33 | Nonce []byte 34 | AuthTag []byte 35 | } 36 | 37 | // Serialize returns the byte serialization of the envelope. 38 | func (e *Envelope) Serialize() []byte { 39 | return encoding.Concat(e.Nonce, e.AuthTag) 40 | } 41 | 42 | func exportKey(conf *internal.Configuration, randomizedPassword, nonce []byte) []byte { 43 | return conf.KDF.Expand(randomizedPassword, encoding.SuffixString(nonce, tag.ExportKey), conf.KDF.Size()) 44 | } 45 | 46 | func authTag(conf *internal.Configuration, randomizedPassword, nonce, ctc []byte) []byte { 47 | authKey := conf.KDF.Expand(randomizedPassword, encoding.SuffixString(nonce, tag.AuthKey), conf.KDF.Size()) 48 | return conf.MAC.MAC(authKey, encoding.Concat(nonce, ctc)) 49 | } 50 | 51 | // cleartextCredentials assumes that clientPublicKey, serverPublicKey are non-nil valid group elements. 52 | func cleartextCredentials(clientPublicKey, serverPublicKey, clientIdentity, serverIdentity []byte) []byte { 53 | if clientIdentity == nil { 54 | clientIdentity = clientPublicKey 55 | } 56 | 57 | if serverIdentity == nil { 58 | serverIdentity = serverPublicKey 59 | } 60 | 61 | return encoding.Concat3( 62 | serverPublicKey, 63 | encoding.EncodeVector(serverIdentity), 64 | encoding.EncodeVector(clientIdentity), 65 | ) 66 | } 67 | 68 | func deriveDiffieHellmanKeyPair( 69 | conf *internal.Configuration, 70 | randomizedPassword, nonce []byte, 71 | ) (*ecc.Scalar, *ecc.Element) { 72 | seed := conf.KDF.Expand(randomizedPassword, encoding.SuffixString(nonce, tag.ExpandPrivateKey), internal.SeedLength) 73 | return oprf.IDFromGroup(conf.Group).DeriveKeyPair(seed, []byte(tag.DeriveDiffieHellmanKeyPair)) 74 | } 75 | 76 | // Store returns the client's Envelope, the masking key for the registration, and the additional export key. 77 | func Store( 78 | conf *internal.Configuration, 79 | randomizedPassword []byte, serverPublicKey *ecc.Element, 80 | credentials *Credentials, 81 | ) (env *Envelope, pku *ecc.Element, export []byte) { 82 | // testing: integrated to support testing with set nonce 83 | nonce := credentials.EnvelopeNonce 84 | if nonce == nil { 85 | nonce = internal.RandomBytes(conf.NonceLen) 86 | } 87 | 88 | _, pku = deriveDiffieHellmanKeyPair(conf, randomizedPassword, nonce) 89 | ctc := cleartextCredentials( 90 | pku.Encode(), 91 | serverPublicKey.Encode(), 92 | credentials.ClientIdentity, 93 | credentials.ServerIdentity, 94 | ) 95 | auth := authTag(conf, randomizedPassword, nonce, ctc) 96 | export = exportKey(conf, randomizedPassword, nonce) 97 | 98 | env = &Envelope{ 99 | Nonce: nonce, 100 | AuthTag: auth, 101 | } 102 | 103 | return env, pku, export 104 | } 105 | 106 | // Recover returns the client's private and public key, as well as the secret export key. 107 | func Recover( 108 | conf *internal.Configuration, 109 | randomizedPassword, serverPublicKey, clientIdentity, serverIdentity []byte, 110 | envelope *Envelope, 111 | ) (clientSecretKey *ecc.Scalar, clientPublicKey *ecc.Element, export []byte, err error) { 112 | clientSecretKey, clientPublicKey = deriveDiffieHellmanKeyPair(conf, randomizedPassword, envelope.Nonce) 113 | ctc := cleartextCredentials( 114 | clientPublicKey.Encode(), 115 | serverPublicKey, 116 | clientIdentity, 117 | serverIdentity, 118 | ) 119 | 120 | expectedTag := authTag(conf, randomizedPassword, envelope.Nonce, ctc) 121 | if !conf.MAC.Equal(expectedTag, envelope.AuthTag) { 122 | return nil, nil, nil, errEnvelopeInvalidMac 123 | } 124 | 125 | export = exportKey(conf, randomizedPassword, envelope.Nonce) 126 | 127 | return clientSecretKey, clientPublicKey, export, nil 128 | } 129 | -------------------------------------------------------------------------------- /internal/keyrecovery/keyrec.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright (C) 2020-2025 Daniel Bourdrez. All Rights Reserved. 4 | // 5 | // This source code is licensed under the MIT license found in the 6 | // LICENSE file in the root directory of this source tree or at 7 | // https://spdx.org/licenses/MIT.html 8 | 9 | package keyrecovery 10 | -------------------------------------------------------------------------------- /internal/masking/masking.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright (C) 2020-2025 Daniel Bourdrez. All Rights Reserved. 4 | // 5 | // This source code is licensed under the MIT license found in the 6 | // LICENSE file in the root directory of this source tree or at 7 | // https://spdx.org/licenses/MIT.html 8 | 9 | // Package masking provides the credential masking mechanism. 10 | package masking 11 | 12 | import ( 13 | "errors" 14 | 15 | "github.com/bytemare/ecc" 16 | 17 | "github.com/bytemare/opaque/internal" 18 | "github.com/bytemare/opaque/internal/encoding" 19 | "github.com/bytemare/opaque/internal/keyrecovery" 20 | "github.com/bytemare/opaque/internal/tag" 21 | ) 22 | 23 | // errUnmaskInvalidPKS happens when the client reads an invalid public key while unmasking. 24 | var errUnmaskInvalidPKS = errors.New("invalid server public key in masked response") 25 | 26 | // Keys contains all the output keys from the masking mechanism. 27 | type Keys struct { 28 | ClientSecretKey *ecc.Scalar 29 | ClientPublicKey, ServerPublicKey *ecc.Element 30 | ExportKey, ServerPublicKeyBytes []byte 31 | } 32 | 33 | // Mask encrypts the serverPublicKey and the envelope under nonceIn and the maskingKey. 34 | func Mask( 35 | conf *internal.Configuration, 36 | nonceIn, maskingKey, serverPublicKey, envelope []byte, 37 | ) (nonce, maskedResponse []byte) { 38 | // testing: integrated to support testing, to force values. 39 | nonce = nonceIn 40 | if len(nonce) == 0 { 41 | nonce = internal.RandomBytes(conf.NonceLen) 42 | } 43 | 44 | clearText := encoding.Concat(serverPublicKey, envelope) 45 | maskedResponse = xorResponse(conf, maskingKey, nonce, clearText) 46 | 47 | return nonce, maskedResponse 48 | } 49 | 50 | // Unmask decrypts the maskedResponse and returns the server's public key and the client key on success. 51 | // This function assumes that maskedResponse has been checked to be of length pointLength + envelope size. 52 | func Unmask( 53 | conf *internal.Configuration, 54 | randomizedPassword, nonce, maskedResponse []byte, 55 | ) (serverPublicKey *ecc.Element, serverPublicKeyBytes []byte, envelope *keyrecovery.Envelope, err error) { 56 | maskingKey := conf.KDF.Expand(randomizedPassword, []byte(tag.MaskingKey), conf.Hash.Size()) 57 | clearText := xorResponse(conf, maskingKey, nonce, maskedResponse) 58 | serverPublicKeyBytes = clearText[:conf.Group.ElementLength()] 59 | env := clearText[conf.Group.ElementLength():] 60 | envelope = &keyrecovery.Envelope{ 61 | Nonce: env[:conf.NonceLen], 62 | AuthTag: env[conf.NonceLen:], 63 | } 64 | 65 | serverPublicKey = conf.Group.NewElement() 66 | if err = serverPublicKey.Decode(serverPublicKeyBytes); err != nil { 67 | return nil, nil, nil, errUnmaskInvalidPKS 68 | } 69 | 70 | return serverPublicKey, serverPublicKeyBytes, envelope, nil 71 | } 72 | 73 | // xorResponse is used to encrypt and decrypt the response in KE2. 74 | // It returns a new byte slice containing the byte-by-byte xor-ing of the in argument and a constructed pad, 75 | // which must be of the same length. 76 | func xorResponse(c *internal.Configuration, key, nonce, in []byte) []byte { 77 | pad := c.KDF.Expand( 78 | key, 79 | encoding.SuffixString(nonce, tag.CredentialResponsePad), 80 | c.Group.ElementLength()+c.EnvelopeSize, 81 | ) 82 | 83 | dst := make([]byte, len(pad)) 84 | 85 | // if the size is fixed, we could unroll the loop 86 | for i, r := range pad { 87 | dst[i] = r ^ in[i] 88 | } 89 | 90 | return dst 91 | } 92 | -------------------------------------------------------------------------------- /internal/oprf/client.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright (C) 2020-2025 Daniel Bourdrez. All Rights Reserved. 4 | // 5 | // This source code is licensed under the MIT license found in the 6 | // LICENSE file in the root directory of this source tree or at 7 | // https://spdx.org/licenses/MIT.html 8 | 9 | package oprf 10 | 11 | import ( 12 | "errors" 13 | 14 | "github.com/bytemare/ecc" 15 | 16 | "github.com/bytemare/opaque/internal/encoding" 17 | "github.com/bytemare/opaque/internal/tag" 18 | ) 19 | 20 | var errInvalidInput = errors.New("invalid input - OPRF input deterministically maps to the group identity element") 21 | 22 | // Client implements the OPRF client and holds its state. 23 | type Client struct { 24 | blind *ecc.Scalar 25 | Identifier 26 | input []byte 27 | } 28 | 29 | // Blind masks the input. 30 | func (c *Client) Blind(input []byte, blind *ecc.Scalar) *ecc.Element { 31 | if blind != nil { 32 | c.blind = blind.Copy() 33 | } else { 34 | c.blind = c.Group().NewScalar().Random() 35 | } 36 | 37 | p := c.Group().HashToGroup(input, c.dst(tag.OPRFPointPrefix)) 38 | if p.IsIdentity() { 39 | panic(errInvalidInput) 40 | } 41 | 42 | c.input = input 43 | 44 | return p.Multiply(c.blind) 45 | } 46 | 47 | func (c *Client) hashTranscript(input, unblinded []byte) []byte { 48 | encInput := encoding.EncodeVector(input) 49 | encElement := encoding.EncodeVector(unblinded) 50 | encDST := []byte(tag.OPRFFinalize) 51 | 52 | return c.hash(encInput, encElement, encDST) 53 | } 54 | 55 | // Finalize terminates the OPRF by unblinding the evaluation and hashing the transcript. 56 | func (c *Client) Finalize(evaluation *ecc.Element) []byte { 57 | invert := c.blind.Copy().Invert() 58 | u := evaluation.Copy().Multiply(invert).Encode() 59 | 60 | return c.hashTranscript(c.input, u) 61 | } 62 | -------------------------------------------------------------------------------- /internal/oprf/oprf.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright (C) 2020-2025 Daniel Bourdrez. All Rights Reserved. 4 | // 5 | // This source code is licensed under the MIT license found in the 6 | // LICENSE file in the root directory of this source tree or at 7 | // https://spdx.org/licenses/MIT.html 8 | 9 | // Package oprf implements the Elliptic Curve Oblivious Pseudorandom Function (EC-OPRF) from 10 | // https://tools.ietf.org/html/draft-irtf-cfrg-voprf. 11 | package oprf 12 | 13 | import ( 14 | "crypto" 15 | 16 | "github.com/bytemare/ecc" 17 | 18 | "github.com/bytemare/opaque/internal/encoding" 19 | "github.com/bytemare/opaque/internal/tag" 20 | ) 21 | 22 | // SeedLength is the default length used for seeds. 23 | const SeedLength = 32 24 | 25 | // Identifier of the OPRF compatible cipher suite to be used. 26 | type Identifier string 27 | 28 | const ( 29 | // Ristretto255Sha512 is the OPRF cipher suite of the Ristretto255 group and SHA-512. 30 | Ristretto255Sha512 Identifier = "ristretto255-SHA512" 31 | 32 | // Decaf448Sha512 is the OPRF cipher suite of the Decaf448 group and SHA-512. 33 | // decaf448Sha512 Identifier = "decaf448-SHAKE256". 34 | 35 | // P256Sha256 is the OPRF cipher suite of the NIST P-256 group and SHA-256. 36 | P256Sha256 Identifier = "P256-SHA256" 37 | 38 | // P384Sha384 is the OPRF cipher suite of the NIST P-384 group and SHA-384. 39 | P384Sha384 Identifier = "P384-SHA384" 40 | 41 | // P521Sha512 is the OPRF cipher suite of the NIST P-512 group and SHA-512. 42 | P521Sha512 Identifier = "P521-SHA512" 43 | 44 | nbIDs = 4 45 | maxDeriveKeyPairTries = 255 46 | ) 47 | 48 | func (i Identifier) dst(prefix string) []byte { 49 | return encoding.Concat([]byte(prefix), i.contextString()) 50 | } 51 | 52 | func (i Identifier) contextString() []byte { 53 | return encoding.Concatenate([]byte(tag.OPRFVersionPrefix), []byte(i)) 54 | } 55 | 56 | func (i Identifier) hash(input ...[]byte) []byte { 57 | h := map[Identifier]crypto.Hash{ 58 | Ristretto255Sha512: crypto.SHA512, 59 | P256Sha256: crypto.SHA256, 60 | P384Sha384: crypto.SHA384, 61 | P521Sha512: crypto.SHA512, 62 | }[i].New() 63 | h.Reset() 64 | 65 | for _, i := range input { 66 | _, _ = h.Write(i) 67 | } 68 | 69 | return h.Sum(nil) 70 | } 71 | 72 | // Available returns whether the Identifier has been registered of not. 73 | func (i Identifier) Available() bool { 74 | // Check for invalid identifiers 75 | return i == Ristretto255Sha512 || 76 | i == P256Sha256 || 77 | i == P384Sha384 || 78 | i == P521Sha512 79 | } 80 | 81 | // IDFromGroup returns the OPRF identifier corresponding to the input ecc. 82 | func IDFromGroup(g ecc.Group) Identifier { 83 | return map[ecc.Group]Identifier{ 84 | ecc.Ristretto255Sha512: Ristretto255Sha512, 85 | ecc.P256Sha256: P256Sha256, 86 | ecc.P384Sha384: P384Sha384, 87 | ecc.P521Sha512: P521Sha512, 88 | }[g] 89 | } 90 | 91 | // Group returns the Group identifier for the cipher suite. 92 | func (i Identifier) Group() ecc.Group { 93 | return map[Identifier]ecc.Group{ 94 | Ristretto255Sha512: ecc.Ristretto255Sha512, 95 | P256Sha256: ecc.P256Sha256, 96 | P384Sha384: ecc.P384Sha384, 97 | P521Sha512: ecc.P521Sha512, 98 | }[i] 99 | } 100 | 101 | // DeriveKey returns a scalar deterministically generated from the input. 102 | func (i Identifier) DeriveKey(seed, info []byte) *ecc.Scalar { 103 | dst := encoding.Concat([]byte(tag.DeriveKeyPairInternal), i.contextString()) 104 | deriveInput := encoding.Concat(seed, encoding.EncodeVector(info)) 105 | 106 | var ( 107 | counter uint8 108 | s *ecc.Scalar 109 | ) 110 | 111 | for s == nil || s.IsZero() { 112 | if counter > maxDeriveKeyPairTries { 113 | panic("DeriveKeyPairError") 114 | } 115 | 116 | s = i.Group().HashToScalar(encoding.Concat(deriveInput, []byte{counter}), dst) 117 | counter++ 118 | } 119 | 120 | return s 121 | } 122 | 123 | // DeriveKeyPair returns a valid keypair deterministically generated from the input. 124 | func (i Identifier) DeriveKeyPair(seed, info []byte) (*ecc.Scalar, *ecc.Element) { 125 | sk := i.DeriveKey(seed, info) 126 | return sk, i.Group().Base().Multiply(sk) 127 | } 128 | 129 | // Client returns an OPRF client. 130 | func (i Identifier) Client() *Client { 131 | return &Client{ 132 | Identifier: i, 133 | input: nil, 134 | blind: nil, 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /internal/oprf/server.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright (C) 2020-2025 Daniel Bourdrez. All Rights Reserved. 4 | // 5 | // This source code is licensed under the MIT license found in the 6 | // LICENSE file in the root directory of this source tree or at 7 | // https://spdx.org/licenses/MIT.html 8 | 9 | package oprf 10 | 11 | import ( 12 | "github.com/bytemare/ecc" 13 | ) 14 | 15 | // Evaluate evaluates the blinded input with the given key. 16 | func (i Identifier) Evaluate(privateKey *ecc.Scalar, blindedElement *ecc.Element) *ecc.Element { 17 | return blindedElement.Copy().Multiply(privateKey) 18 | } 19 | -------------------------------------------------------------------------------- /internal/tag/strings.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright (C) 2020-2025 Daniel Bourdrez. All Rights Reserved. 4 | // 5 | // This source code is licensed under the MIT license found in the 6 | // LICENSE file in the root directory of this source tree or at 7 | // https://spdx.org/licenses/MIT.html 8 | 9 | // Package tag provides the static tag strings to OPAQUE. 10 | package tag 11 | 12 | // These strings are the static tags and labels used throughout the protocol. 13 | const ( 14 | // OPRF tags. 15 | 16 | // OPRFVersionPrefix is a string explicitly stating the version name. 17 | OPRFVersionPrefix = "OPRFV1-\x00-" 18 | 19 | // DeriveKeyPairInternal is the internal DeriveKeyPair tag as defined in VOPRF. 20 | DeriveKeyPairInternal = "DeriveKeyPair" 21 | 22 | // OPRFPointPrefix is the DST prefix to use for HashToGroup operations. 23 | OPRFPointPrefix = "HashToGroup-" 24 | 25 | // OPRFFinalize is the DST suffix used in the client transcript. 26 | OPRFFinalize = "Finalize" 27 | 28 | // Envelope tags. 29 | 30 | // AuthKey is the envelope's MAC key's KDF dst. 31 | AuthKey = "AuthKey" 32 | 33 | // ExportKey is the export key's KDF dst. 34 | ExportKey = "ExportKey" 35 | 36 | // MaskingKey is the masking key's creation KDF dst. 37 | MaskingKey = "MaskingKey" 38 | 39 | // DeriveDiffieHellmanKeyPair is the private key hash-to-scalar dst. 40 | DeriveDiffieHellmanKeyPair = "OPAQUE-DeriveDiffieHellmanKeyPair" 41 | 42 | // ExpandPrivateKey is the client's private key seed KDF dst. 43 | ExpandPrivateKey = "PrivateKey" 44 | 45 | // 3DH tags. 46 | 47 | // VersionTag indicates the protocol RFC identifier for the AKE transcript prefix. 48 | VersionTag = "OPAQUEv1-" 49 | 50 | // LabelPrefix is the 3DH secret KDF dst prefix. 51 | LabelPrefix = "OPAQUE-" 52 | 53 | // Handshake is the 3DH HandshakeSecret dst. 54 | Handshake = "HandshakeSecret" 55 | 56 | // SessionKey is the 3DH session secret dst. 57 | SessionKey = "SessionKey" 58 | 59 | // MacServer is 3DH server's MAC key KDF dst. 60 | MacServer = "ServerMAC" 61 | 62 | // MacClient is 3DH server's MAC key KDF dst. 63 | MacClient = "ClientMAC" 64 | 65 | // Client tags. 66 | 67 | // CredentialResponsePad is the masking keys KDF dst to expand to the input. 68 | CredentialResponsePad = "CredentialResponsePad" 69 | 70 | // Server tags. 71 | 72 | // ExpandOPRF is the server's OPRF key seed KDF dst. 73 | ExpandOPRF = "OprfKey" 74 | 75 | // DeriveKeyPair is the server's OPRF hash-to-scalar dst. 76 | DeriveKeyPair = "OPAQUE-DeriveKeyPair" 77 | ) 78 | -------------------------------------------------------------------------------- /message/credentials.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright (C) 2020-2025 Daniel Bourdrez. All Rights Reserved. 4 | // 5 | // This source code is licensed under the MIT license found in the 6 | // LICENSE file in the root directory of this source tree or at 7 | // https://spdx.org/licenses/MIT.html 8 | 9 | package message 10 | 11 | import ( 12 | "github.com/bytemare/ecc" 13 | 14 | "github.com/bytemare/opaque/internal/encoding" 15 | ) 16 | 17 | // CredentialRequest represents a credential request message. 18 | type CredentialRequest struct { 19 | BlindedMessage *ecc.Element `json:"blindedMessage"` 20 | } 21 | 22 | // NewCredentialRequest returns a populated CredentialRequest. 23 | func NewCredentialRequest(blindedMessage *ecc.Element) *CredentialRequest { 24 | return &CredentialRequest{ 25 | BlindedMessage: blindedMessage, 26 | } 27 | } 28 | 29 | // Serialize returns the byte encoding of CredentialRequest. 30 | func (c *CredentialRequest) Serialize() []byte { 31 | return c.BlindedMessage.Encode() 32 | } 33 | 34 | // CredentialResponse represents a credential response message. 35 | type CredentialResponse struct { 36 | EvaluatedMessage *ecc.Element `json:"evaluatedMessage"` 37 | MaskingNonce []byte `json:"maskingNonce"` 38 | MaskedResponse []byte `json:"maskedResponse"` 39 | } 40 | 41 | // NewCredentialResponse returns a populated CredentialResponse. 42 | func NewCredentialResponse(message *ecc.Element, nonce, response []byte) *CredentialResponse { 43 | return &CredentialResponse{ 44 | EvaluatedMessage: message, 45 | MaskingNonce: nonce, 46 | MaskedResponse: response, 47 | } 48 | } 49 | 50 | // Serialize returns the byte encoding of CredentialResponse. 51 | func (c *CredentialResponse) Serialize() []byte { 52 | return encoding.Concat3(c.EvaluatedMessage.Encode(), c.MaskingNonce, c.MaskedResponse) 53 | } 54 | -------------------------------------------------------------------------------- /message/login.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright (C) 2020-2025 Daniel Bourdrez. All Rights Reserved. 4 | // 5 | // This source code is licensed under the MIT license found in the 6 | // LICENSE file in the root directory of this source tree or at 7 | // https://spdx.org/licenses/MIT.html 8 | 9 | // Package message provides message structures for the OPAQUE protocol. 10 | package message 11 | 12 | import ( 13 | "github.com/bytemare/ecc" 14 | 15 | "github.com/bytemare/opaque/internal/encoding" 16 | ) 17 | 18 | // KE1 is the first message of the login flow, created by the client and sent to the server. 19 | type KE1 struct { 20 | *CredentialRequest 21 | ClientPublicKeyshare *ecc.Element `json:"clientPublicKeyshare"` 22 | ClientNonce []byte `json:"clientNonce"` 23 | } 24 | 25 | // Serialize returns the byte encoding of KE1. 26 | func (m *KE1) Serialize() []byte { 27 | return encoding.Concat3(m.CredentialRequest.Serialize(), m.ClientNonce, m.ClientPublicKeyshare.Encode()) 28 | } 29 | 30 | // KE2 is the second message of the login flow, created by the server and sent to the client. 31 | type KE2 struct { 32 | *CredentialResponse 33 | ServerPublicKeyshare *ecc.Element `json:"serverPublicKeyshare"` 34 | ServerNonce []byte `json:"serverNonce"` 35 | ServerMac []byte `json:"serverMac"` 36 | } 37 | 38 | // Serialize returns the byte encoding of KE2. 39 | func (m *KE2) Serialize() []byte { 40 | return encoding.Concat( 41 | m.CredentialResponse.Serialize(), 42 | encoding.Concat3(m.ServerNonce, m.ServerPublicKeyshare.Encode(), m.ServerMac), 43 | ) 44 | } 45 | 46 | // KE3 is the third and last message of the login flow, created by the client and sent to the server. 47 | type KE3 struct { 48 | ClientMac []byte `json:"clientMac"` 49 | } 50 | 51 | // Serialize returns the byte encoding of KE3. 52 | func (k KE3) Serialize() []byte { 53 | return k.ClientMac 54 | } 55 | -------------------------------------------------------------------------------- /message/registration.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright (C) 2020-2025 Daniel Bourdrez. All Rights Reserved. 4 | // 5 | // This source code is licensed under the MIT license found in the 6 | // LICENSE file in the root directory of this source tree or at 7 | // https://spdx.org/licenses/MIT.html 8 | 9 | package message 10 | 11 | import ( 12 | "github.com/bytemare/ecc" 13 | 14 | "github.com/bytemare/opaque/internal/encoding" 15 | ) 16 | 17 | // RegistrationRequest is the first message of the registration flow, created by the client and sent to the server. 18 | type RegistrationRequest struct { 19 | BlindedMessage *ecc.Element `json:"blindedMessage"` 20 | } 21 | 22 | // Serialize returns the byte encoding of RegistrationRequest. 23 | func (r *RegistrationRequest) Serialize() []byte { 24 | return r.BlindedMessage.Encode() 25 | } 26 | 27 | // RegistrationResponse is the second message of the registration flow, created by the server and sent to the client. 28 | type RegistrationResponse struct { 29 | EvaluatedMessage *ecc.Element `json:"evaluatedMessage"` 30 | Pks *ecc.Element `json:"serverPublicKey"` 31 | } 32 | 33 | // Serialize returns the byte encoding of RegistrationResponse. 34 | func (r *RegistrationResponse) Serialize() []byte { 35 | return encoding.Concat(r.EvaluatedMessage.Encode(), r.Pks.Encode()) 36 | } 37 | 38 | // RegistrationRecord represents the client record sent as the last registration message by the client to the server. 39 | type RegistrationRecord struct { 40 | PublicKey *ecc.Element `json:"clientPublicKey"` 41 | MaskingKey []byte `json:"maskingKey"` 42 | Envelope []byte `json:"envelope"` 43 | } 44 | 45 | // Serialize returns the byte encoding of RegistrationRecord. 46 | func (r *RegistrationRecord) Serialize() []byte { 47 | return encoding.Concat3(r.PublicKey.Encode(), r.MaskingKey, r.Envelope) 48 | } 49 | -------------------------------------------------------------------------------- /opaque.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright (C) 2020-2025 Daniel Bourdrez. All Rights Reserved. 4 | // 5 | // This source code is licensed under the MIT license found in the 6 | // LICENSE file in the root directory of this source tree or at 7 | // https://spdx.org/licenses/MIT.html 8 | 9 | // Package opaque implements OPAQUE, an asymmetric password-authenticated key exchange protocol that is secure against 10 | // pre-computation attacks. It enables a client to authenticate to a server without ever revealing its password to the 11 | // server. Protocol details can be found on the IETF RFC page (https://datatracker.ietf.org/doc/draft-irtf-cfrg-opaque) 12 | // and on the GitHub specification repository (https://github.com/cfrg/draft-irtf-cfrg-opaque). 13 | package opaque 14 | 15 | import ( 16 | "crypto" 17 | "errors" 18 | "fmt" 19 | 20 | "github.com/bytemare/ecc" 21 | "github.com/bytemare/hash" 22 | "github.com/bytemare/ksf" 23 | 24 | "github.com/bytemare/opaque/internal" 25 | "github.com/bytemare/opaque/internal/ake" 26 | "github.com/bytemare/opaque/internal/encoding" 27 | "github.com/bytemare/opaque/internal/oprf" 28 | "github.com/bytemare/opaque/message" 29 | ) 30 | 31 | // Group identifies the prime-order group with hash-to-curve capability to use in OPRF and AKE. 32 | type Group byte 33 | 34 | const ( 35 | // RistrettoSha512 identifies the Ristretto255 group and SHA-512. 36 | RistrettoSha512 = Group(ecc.Ristretto255Sha512) 37 | 38 | // decaf448Shake256 identifies the Decaf448 group and Shake-256. 39 | // decaf448Shake256 = 2. 40 | 41 | // P256Sha256 identifies the NIST P-256 group and SHA-256. 42 | P256Sha256 = Group(ecc.P256Sha256) 43 | 44 | // P384Sha512 identifies the NIST P-384 group and SHA-384. 45 | P384Sha512 = Group(ecc.P384Sha384) 46 | 47 | // P521Sha512 identifies the NIST P-512 group and SHA-512. 48 | P521Sha512 = Group(ecc.P521Sha512) 49 | ) 50 | 51 | // Available returns whether the Group byte is recognized in this implementation. This allows to fail early when 52 | // working with multiple versions not using the same configuration and ecc. 53 | func (g Group) Available() bool { 54 | return g == RistrettoSha512 || 55 | g == P256Sha256 || 56 | g == P384Sha512 || 57 | g == P521Sha512 58 | } 59 | 60 | // OPRF returns the OPRF Identifier used in the Ciphersuite. 61 | func (g Group) OPRF() oprf.Identifier { 62 | return oprf.IDFromGroup(g.Group()) 63 | } 64 | 65 | // Group returns the EC Group used in the Ciphersuite. 66 | func (g Group) Group() ecc.Group { 67 | return ecc.Group(g) 68 | } 69 | 70 | const confIDsLength = 6 71 | 72 | var ( 73 | errInvalidOPRFid = errors.New("invalid OPRF group id") 74 | errInvalidKDFid = errors.New("invalid KDF id") 75 | errInvalidMACid = errors.New("invalid MAC id") 76 | errInvalidHASHid = errors.New("invalid Hash id") 77 | errInvalidKSFid = errors.New("invalid KSF id") 78 | errInvalidAKEid = errors.New("invalid AKE group id") 79 | ) 80 | 81 | // Configuration represents an OPAQUE configuration. Note that OprfGroup and AKEGroup are recommended to be the same, 82 | // as well as KDF, MAC, Hash should be the same. 83 | type Configuration struct { 84 | Context []byte 85 | KDF crypto.Hash `json:"kdf"` 86 | MAC crypto.Hash `json:"mac"` 87 | Hash crypto.Hash `json:"hash"` 88 | KSF ksf.Identifier `json:"ksf"` 89 | OPRF Group `json:"oprf"` 90 | AKE Group `json:"group"` 91 | } 92 | 93 | // DefaultConfiguration returns a default configuration with strong parameters. 94 | func DefaultConfiguration() *Configuration { 95 | return &Configuration{ 96 | OPRF: RistrettoSha512, 97 | AKE: RistrettoSha512, 98 | KSF: ksf.Argon2id, 99 | KDF: crypto.SHA512, 100 | MAC: crypto.SHA512, 101 | Hash: crypto.SHA512, 102 | Context: nil, 103 | } 104 | } 105 | 106 | // Client returns a newly instantiated Client from the Configuration. 107 | func (c *Configuration) Client() (*Client, error) { 108 | return NewClient(c) 109 | } 110 | 111 | // Server returns a newly instantiated Server from the Configuration. 112 | func (c *Configuration) Server() (*Server, error) { 113 | return NewServer(c) 114 | } 115 | 116 | // GenerateOPRFSeed returns a OPRF seed valid in the given configuration. 117 | func (c *Configuration) GenerateOPRFSeed() []byte { 118 | return RandomBytes(c.Hash.Size()) 119 | } 120 | 121 | // KeyGen returns a key pair in the AKE ecc. 122 | func (c *Configuration) KeyGen() (secretKey, publicKey []byte) { 123 | return ake.KeyGen(ecc.Group(c.AKE)) 124 | } 125 | 126 | // verify returns an error on the first non-compliant parameter, nil otherwise. 127 | func (c *Configuration) verify() error { 128 | if !c.OPRF.Available() || !c.OPRF.OPRF().Available() { 129 | return errInvalidOPRFid 130 | } 131 | 132 | if !c.AKE.Available() || !c.AKE.Group().Available() { 133 | return errInvalidAKEid 134 | } 135 | 136 | if c.KDF >= 25 || !hash.Hash(c.KDF).Available() { //nolint:gosec // overflow is checked beforehand. 137 | return errInvalidKDFid 138 | } 139 | 140 | if c.MAC >= 25 || !hash.Hash(c.MAC).Available() { //nolint:gosec // overflow is checked beforehand. 141 | return errInvalidMACid 142 | } 143 | 144 | if c.Hash >= 25 || !hash.Hash(c.Hash).Available() { //nolint:gosec // overflow is checked beforehand. 145 | return errInvalidHASHid 146 | } 147 | 148 | if c.KSF != 0 && !c.KSF.Available() { 149 | return errInvalidKSFid 150 | } 151 | 152 | return nil 153 | } 154 | 155 | // toInternal builds the internal representation of the configuration parameters. 156 | func (c *Configuration) toInternal() (*internal.Configuration, error) { 157 | if err := c.verify(); err != nil { 158 | return nil, err 159 | } 160 | 161 | g := c.AKE.Group() 162 | o := c.OPRF.OPRF() 163 | mac := internal.NewMac(c.MAC) 164 | ip := &internal.Configuration{ 165 | OPRF: o, 166 | Group: g, 167 | KSF: internal.NewKSF(c.KSF), 168 | KDF: internal.NewKDF(c.KDF), 169 | MAC: mac, 170 | Hash: internal.NewHash(c.Hash), 171 | NonceLen: internal.NonceLength, 172 | EnvelopeSize: internal.NonceLength + mac.Size(), 173 | Context: c.Context, 174 | } 175 | 176 | return ip, nil 177 | } 178 | 179 | // Deserializer returns a pointer to a Deserializer structure allowing deserialization of messages in the given 180 | // configuration. 181 | func (c *Configuration) Deserializer() (*Deserializer, error) { 182 | conf, err := c.toInternal() 183 | if err != nil { 184 | return nil, err 185 | } 186 | 187 | return &Deserializer{conf: conf}, nil 188 | } 189 | 190 | // Serialize returns the byte encoding of the Configuration structure. 191 | func (c *Configuration) Serialize() []byte { 192 | ids := []byte{ 193 | byte(c.OPRF), 194 | byte(c.AKE), 195 | byte(c.KSF), 196 | byte(c.KDF), 197 | byte(c.MAC), 198 | byte(c.Hash), 199 | } 200 | 201 | return encoding.Concatenate(ids, encoding.EncodeVector(c.Context)) 202 | } 203 | 204 | // DeserializeConfiguration decodes the input and returns a Parameter structure. 205 | func DeserializeConfiguration(encoded []byte) (*Configuration, error) { 206 | // corresponds to the configuration length + 2-byte encoding of empty context 207 | if len(encoded) < confIDsLength+2 { 208 | return nil, internal.ErrConfigurationInvalidLength 209 | } 210 | 211 | ctx, _, err := encoding.DecodeVector(encoded[confIDsLength:]) 212 | if err != nil { 213 | return nil, fmt.Errorf("decoding the configuration context: %w", err) 214 | } 215 | 216 | c := &Configuration{ 217 | OPRF: Group(encoded[0]), 218 | AKE: Group(encoded[1]), 219 | KSF: ksf.Identifier(encoded[2]), 220 | KDF: crypto.Hash(encoded[3]), 221 | MAC: crypto.Hash(encoded[4]), 222 | Hash: crypto.Hash(encoded[5]), 223 | Context: ctx, 224 | } 225 | 226 | if err2 := c.verify(); err2 != nil { 227 | return nil, err2 228 | } 229 | 230 | return c, nil 231 | } 232 | 233 | // GetFakeRecord creates a fake Client record to be used when no existing client record exists, 234 | // to defend against client enumeration techniques. 235 | func (c *Configuration) GetFakeRecord(credentialIdentifier []byte) (*ClientRecord, error) { 236 | i, err := c.toInternal() 237 | if err != nil { 238 | return nil, err 239 | } 240 | 241 | scalar := i.Group.NewScalar().Random() 242 | publicKey := i.Group.Base().Multiply(scalar) 243 | 244 | regRecord := &message.RegistrationRecord{ 245 | PublicKey: publicKey, 246 | MaskingKey: RandomBytes(i.KDF.Size()), 247 | Envelope: make([]byte, internal.NonceLength+i.MAC.Size()), 248 | } 249 | 250 | return &ClientRecord{ 251 | CredentialIdentifier: credentialIdentifier, 252 | ClientIdentity: nil, 253 | RegistrationRecord: regRecord, 254 | }, nil 255 | } 256 | 257 | // ClientRecord is a server-side structure enabling the storage of user relevant information. 258 | type ClientRecord struct { 259 | *message.RegistrationRecord 260 | CredentialIdentifier []byte 261 | ClientIdentity []byte 262 | } 263 | 264 | // RandomBytes returns random bytes of length len (wrapper for crypto/rand). 265 | func RandomBytes(length int) []byte { 266 | return internal.RandomBytes(length) 267 | } 268 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright (C) 2020-2025 Daniel Bourdrez. All Rights Reserved. 4 | // 5 | // This source code is licensed under the MIT license found in the 6 | // LICENSE file in the root directory of this source tree or at 7 | // https://spdx.org/licenses/MIT.html 8 | 9 | package opaque 10 | 11 | import ( 12 | "errors" 13 | "fmt" 14 | 15 | "github.com/bytemare/ecc" 16 | 17 | "github.com/bytemare/opaque/internal" 18 | "github.com/bytemare/opaque/internal/ake" 19 | "github.com/bytemare/opaque/internal/encoding" 20 | "github.com/bytemare/opaque/internal/masking" 21 | "github.com/bytemare/opaque/internal/tag" 22 | "github.com/bytemare/opaque/message" 23 | ) 24 | 25 | var ( 26 | // ErrNoServerKeyMaterial indicates that the server's key material has not been set. 27 | ErrNoServerKeyMaterial = errors.New("key material not set: call SetKeyMaterial() to set values") 28 | 29 | // ErrAkeInvalidClientMac indicates that the MAC contained in the KE3 message is not valid in the given session. 30 | ErrAkeInvalidClientMac = errors.New("failed to authenticate client: invalid client mac") 31 | 32 | // ErrInvalidState indicates that the given state is not valid due to a wrong length. 33 | ErrInvalidState = errors.New("invalid state length") 34 | 35 | // ErrInvalidEnvelopeLength indicates the envelope contained in the record is of invalid length. 36 | ErrInvalidEnvelopeLength = errors.New("record has invalid envelope length") 37 | 38 | // ErrInvalidPksLength indicates the input public key is not of right length. 39 | ErrInvalidPksLength = errors.New("input server public key's length is invalid") 40 | 41 | // ErrInvalidOPRFSeedLength indicates that the OPRF seed is not of right length. 42 | ErrInvalidOPRFSeedLength = errors.New("input OPRF seed length is invalid (must be of hash output length)") 43 | 44 | // ErrZeroSKS indicates that the server's private key is a zero scalar. 45 | ErrZeroSKS = errors.New("server private key is zero") 46 | ) 47 | 48 | // Server represents an OPAQUE Server, exposing its functions and holding its state. 49 | type Server struct { 50 | Deserialize *Deserializer 51 | conf *internal.Configuration 52 | Ake *ake.Server 53 | *keyMaterial 54 | } 55 | 56 | type keyMaterial struct { 57 | serverIdentity []byte 58 | serverSecretKey *ecc.Scalar 59 | serverPublicKey []byte 60 | oprfSeed []byte 61 | } 62 | 63 | // NewServer returns a Server instantiation given the application Configuration. 64 | func NewServer(c *Configuration) (*Server, error) { 65 | if c == nil { 66 | c = DefaultConfiguration() 67 | } 68 | 69 | conf, err := c.toInternal() 70 | if err != nil { 71 | return nil, err 72 | } 73 | 74 | return &Server{ 75 | Deserialize: &Deserializer{conf: conf}, 76 | conf: conf, 77 | Ake: ake.NewServer(), 78 | keyMaterial: nil, 79 | }, nil 80 | } 81 | 82 | // GetConf return the internal configuration. 83 | func (s *Server) GetConf() *internal.Configuration { 84 | return s.conf 85 | } 86 | 87 | func (s *Server) oprfResponse(element *ecc.Element, oprfSeed, credentialIdentifier []byte) *ecc.Element { 88 | seed := s.conf.KDF.Expand( 89 | oprfSeed, 90 | encoding.SuffixString(credentialIdentifier, tag.ExpandOPRF), 91 | internal.SeedLength, 92 | ) 93 | ku := s.conf.OPRF.DeriveKey(seed, []byte(tag.DeriveKeyPair)) 94 | 95 | return s.conf.OPRF.Evaluate(ku, element) 96 | } 97 | 98 | // RegistrationResponse returns a RegistrationResponse message to the input RegistrationRequest message and given 99 | // identifiers. 100 | func (s *Server) RegistrationResponse( 101 | req *message.RegistrationRequest, 102 | serverPublicKey *ecc.Element, 103 | credentialIdentifier, oprfSeed []byte, 104 | ) *message.RegistrationResponse { 105 | z := s.oprfResponse(req.BlindedMessage, oprfSeed, credentialIdentifier) 106 | 107 | return &message.RegistrationResponse{ 108 | EvaluatedMessage: z, 109 | Pks: serverPublicKey, 110 | } 111 | } 112 | 113 | func (s *Server) credentialResponse( 114 | req *message.CredentialRequest, 115 | serverPublicKey []byte, 116 | record *message.RegistrationRecord, 117 | credentialIdentifier, oprfSeed, maskingNonce []byte, 118 | ) *message.CredentialResponse { 119 | z := s.oprfResponse(req.BlindedMessage, oprfSeed, credentialIdentifier) 120 | 121 | maskingNonce, maskedResponse := masking.Mask( 122 | s.conf, 123 | maskingNonce, 124 | record.MaskingKey, 125 | serverPublicKey, 126 | record.Envelope, 127 | ) 128 | 129 | return message.NewCredentialResponse(z, maskingNonce, maskedResponse) 130 | } 131 | 132 | // GenerateKE2Options enable setting optional values for the session, which default to secure random values if not 133 | // set. 134 | type GenerateKE2Options struct { 135 | // KeyShareSeed: optional. 136 | KeyShareSeed []byte 137 | // AKENonce: optional. 138 | AKENonce []byte 139 | // MaskingNonce: optional. 140 | MaskingNonce []byte 141 | // AKENonceLength: optional, overrides the default length of the nonce to be created if no nonce is provided. 142 | AKENonceLength uint32 143 | } 144 | 145 | func getGenerateKE2Options(options []GenerateKE2Options) (*ake.Options, []byte) { 146 | var ( 147 | op ake.Options 148 | maskingNonce []byte 149 | ) 150 | 151 | if len(options) != 0 { 152 | op.KeyShareSeed = options[0].KeyShareSeed 153 | op.Nonce = options[0].AKENonce 154 | op.NonceLength = options[0].AKENonceLength 155 | maskingNonce = options[0].MaskingNonce 156 | } 157 | 158 | return &op, maskingNonce 159 | } 160 | 161 | // SetKeyMaterial set the server's identity and mandatory key material to be used during GenerateKE2(). 162 | // All these values must be the same as used during client registration and remain the same across protocol execution 163 | // for a given registered client. 164 | // 165 | // - serverIdentity can be nil, in which case it will be set to serverPublicKey. 166 | // - serverSecretKey is the server's secret AKE key. 167 | // - serverPublicKey is the server's public AKE key to the serverSecretKey. 168 | // - oprfSeed is the long-term OPRF input seed. 169 | func (s *Server) SetKeyMaterial(serverIdentity, serverSecretKey, serverPublicKey, oprfSeed []byte) error { 170 | sks := s.conf.Group.NewScalar() 171 | if err := sks.Decode(serverSecretKey); err != nil { 172 | return fmt.Errorf("invalid server AKE secret key: %w", err) 173 | } 174 | 175 | if sks.IsZero() { 176 | return ErrZeroSKS 177 | } 178 | 179 | if len(oprfSeed) != s.conf.Hash.Size() { 180 | return ErrInvalidOPRFSeedLength 181 | } 182 | 183 | if len(serverPublicKey) != s.conf.Group.ElementLength() { 184 | return ErrInvalidPksLength 185 | } 186 | 187 | if err := s.conf.Group.NewElement().Decode(serverPublicKey); err != nil { 188 | return fmt.Errorf("invalid server public key: %w", err) 189 | } 190 | 191 | s.keyMaterial = &keyMaterial{ 192 | serverIdentity: serverIdentity, 193 | serverSecretKey: sks, 194 | serverPublicKey: serverPublicKey, 195 | oprfSeed: oprfSeed, 196 | } 197 | 198 | return nil 199 | } 200 | 201 | // GenerateKE2 responds to a KE1 message with a KE2 message a client record. 202 | func (s *Server) GenerateKE2( 203 | ke1 *message.KE1, 204 | record *ClientRecord, 205 | options ...GenerateKE2Options, 206 | ) (*message.KE2, error) { 207 | if s.keyMaterial == nil { 208 | return nil, ErrNoServerKeyMaterial 209 | } 210 | 211 | if len(record.Envelope) != s.conf.EnvelopeSize { 212 | return nil, ErrInvalidEnvelopeLength 213 | } 214 | 215 | // We've checked that the server's public key and the client's envelope are of correct length, 216 | // thus ensuring that the subsequent xor-ing input is the same length as the encryption pad. 217 | 218 | op, maskingNonce := getGenerateKE2Options(options) 219 | 220 | response := s.credentialResponse(ke1.CredentialRequest, s.serverPublicKey, 221 | record.RegistrationRecord, record.CredentialIdentifier, s.oprfSeed, maskingNonce) 222 | 223 | identities := ake.Identities{ 224 | ClientIdentity: record.ClientIdentity, 225 | ServerIdentity: s.serverIdentity, 226 | } 227 | identities.SetIdentities(record.PublicKey, s.serverPublicKey) 228 | 229 | ke2 := s.Ake.Response(s.conf, &identities, s.serverSecretKey, record.PublicKey, ke1, response, *op) 230 | 231 | return ke2, nil 232 | } 233 | 234 | // LoginFinish returns an error if the KE3 received from the client holds an invalid mac, and nil if correct. 235 | func (s *Server) LoginFinish(ke3 *message.KE3) error { 236 | if !s.Ake.Finalize(s.conf, ke3) { 237 | return ErrAkeInvalidClientMac 238 | } 239 | 240 | return nil 241 | } 242 | 243 | // SessionKey returns the session key if the previous call to GenerateKE2() was successful. 244 | func (s *Server) SessionKey() []byte { 245 | return s.Ake.SessionKey() 246 | } 247 | 248 | // ExpectedMAC returns the expected client MAC if the previous call to GenerateKE2() was successful. 249 | func (s *Server) ExpectedMAC() []byte { 250 | return s.Ake.ExpectedMAC() 251 | } 252 | 253 | // SetAKEState sets the internal state of the AKE server from the given bytes. 254 | func (s *Server) SetAKEState(state []byte) error { 255 | if len(state) != s.conf.MAC.Size()+s.conf.KDF.Size() { 256 | return ErrInvalidState 257 | } 258 | 259 | if err := s.Ake.SetState(state[:s.conf.MAC.Size()], state[s.conf.MAC.Size():]); err != nil { 260 | return fmt.Errorf("setting AKE state: %w", err) 261 | } 262 | 263 | return nil 264 | } 265 | 266 | // SerializeState returns the internal state of the AKE server serialized to bytes. 267 | func (s *Server) SerializeState() []byte { 268 | return s.Ake.SerializeState() 269 | } 270 | -------------------------------------------------------------------------------- /tests/client_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright (C) 2020-2025 Daniel Bourdrez. All Rights Reserved. 4 | // 5 | // This source code is licensed under the MIT license found in the 6 | // LICENSE file in the root directory of this source tree or at 7 | // https://spdx.org/licenses/MIT.html 8 | 9 | package opaque_test 10 | 11 | import ( 12 | "crypto" 13 | "encoding/hex" 14 | "log" 15 | "strings" 16 | "testing" 17 | 18 | "github.com/bytemare/ksf" 19 | 20 | "github.com/bytemare/opaque" 21 | "github.com/bytemare/opaque/internal" 22 | "github.com/bytemare/opaque/internal/encoding" 23 | "github.com/bytemare/opaque/internal/tag" 24 | ) 25 | 26 | /* 27 | The following tests look for failing conditions. 28 | */ 29 | 30 | func TestClientRegistrationFinalize_InvalidPks(t *testing.T) { 31 | /* 32 | Invalid data sent to the client 33 | */ 34 | credID := internal.RandomBytes(32) 35 | 36 | testAll(t, func(t2 *testing.T, conf *configuration) { 37 | client, err := conf.conf.Client() 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | 42 | server, err := conf.conf.Server() 43 | if err != nil { 44 | t.Fatal(err) 45 | } 46 | 47 | _, pks := conf.conf.KeyGen() 48 | oprfSeed := internal.RandomBytes(conf.conf.Hash.Size()) 49 | r1 := client.RegistrationInit([]byte("yo")) 50 | 51 | pk := server.GetConf().Group.NewElement() 52 | if err = pk.Decode(pks); err != nil { 53 | panic(err) 54 | } 55 | r2 := server.RegistrationResponse(r1, pk, credID, oprfSeed) 56 | 57 | // message length 58 | badr2 := internal.RandomBytes(15) 59 | expected := "invalid message length" 60 | if _, err = client.Deserialize.RegistrationResponse(badr2); err == nil || 61 | !strings.HasPrefix(err.Error(), expected) { 62 | t.Fatalf("expected error for empty server public key - got %v", err) 63 | } 64 | 65 | // invalid data 66 | badr2 = encoding.Concat(getBadElement(t, conf), pks) 67 | expected = "invalid OPRF evaluation" 68 | if _, err = client.Deserialize.RegistrationResponse(badr2); err == nil || 69 | !strings.HasPrefix(err.Error(), expected) { 70 | t.Fatalf("expected error for empty server public key - got %v", err) 71 | } 72 | 73 | // nil pks 74 | expected = "invalid server public key" 75 | badr2 = encoding.Concat(r2.Serialize()[:client.GetConf().OPRF.Group().ElementLength()], getBadElement(t, conf)) 76 | if _, err = client.Deserialize.RegistrationResponse(badr2); err == nil || 77 | !strings.HasPrefix(err.Error(), expected) { 78 | t.Fatalf("expected error for invalid server public key - got %v", err) 79 | } 80 | }) 81 | } 82 | 83 | func TestClientFinish_BadEvaluation(t *testing.T) { 84 | /* 85 | Oprf finalize : evaluation deserialization // element decoding 86 | */ 87 | testAll(t, func(t2 *testing.T, conf *configuration) { 88 | client, err := conf.conf.Client() 89 | if err != nil { 90 | t.Fatal(err) 91 | } 92 | 93 | _ = client.GenerateKE1([]byte("yo")) 94 | r2 := encoding.Concat( 95 | getBadElement(t, conf), 96 | internal.RandomBytes( 97 | client.GetConf().NonceLen+client.GetConf().Group.ElementLength()+client.GetConf().EnvelopeSize, 98 | ), 99 | ) 100 | badKe2 := encoding.Concat( 101 | r2, 102 | internal.RandomBytes( 103 | client.GetConf().NonceLen+client.GetConf().Group.ElementLength()+client.GetConf().MAC.Size(), 104 | ), 105 | ) 106 | 107 | expected := "invalid OPRF evaluation" 108 | if _, err = client.Deserialize.KE2(badKe2); err == nil || !strings.HasPrefix(err.Error(), expected) { 109 | t.Fatalf("expected error for invalid evaluated element - got %v", err) 110 | } 111 | }) 112 | } 113 | 114 | func TestClientFinish_BadMaskedResponse(t *testing.T) { 115 | /* 116 | The masked response is of invalid length. 117 | */ 118 | credID := internal.RandomBytes(32) 119 | 120 | testAll(t, func(t2 *testing.T, conf *configuration) { 121 | client, err := conf.conf.Client() 122 | if err != nil { 123 | t.Fatal(err) 124 | } 125 | 126 | server, err := conf.conf.Server() 127 | if err != nil { 128 | t.Fatal(err) 129 | } 130 | 131 | sks, pks := conf.conf.KeyGen() 132 | oprfSeed := internal.RandomBytes(conf.conf.Hash.Size()) 133 | 134 | if err = server.SetKeyMaterial(nil, sks, pks, oprfSeed); err != nil { 135 | t.Fatal(err) 136 | } 137 | 138 | rec := buildRecord(credID, oprfSeed, []byte("yo"), pks, client, server) 139 | 140 | ke1 := client.GenerateKE1([]byte("yo")) 141 | ke2, _ := server.GenerateKE2(ke1, rec) 142 | 143 | goodLength := client.GetConf().Group.ElementLength() + client.GetConf().EnvelopeSize 144 | expected := "invalid masked response length" 145 | 146 | // too short 147 | ke2.MaskedResponse = internal.RandomBytes(goodLength - 1) 148 | if _, _, err = client.GenerateKE3(ke2); err == nil || !strings.HasPrefix(err.Error(), expected) { 149 | t.Fatalf("expected error for short response - got %v", err) 150 | } 151 | 152 | // too long 153 | ke2.MaskedResponse = internal.RandomBytes(goodLength + 1) 154 | if _, _, err = client.GenerateKE3(ke2); err == nil || !strings.HasPrefix(err.Error(), expected) { 155 | t.Fatalf("expected error for long response - got %v", err) 156 | } 157 | }) 158 | } 159 | 160 | func TestClientFinish_InvalidEnvelopeTag(t *testing.T) { 161 | /* 162 | Invalid envelope tag 163 | */ 164 | credID := internal.RandomBytes(32) 165 | 166 | testAll(t, func(t2 *testing.T, conf *configuration) { 167 | client, err := conf.conf.Client() 168 | if err != nil { 169 | t.Fatal(err) 170 | } 171 | 172 | server, err := conf.conf.Server() 173 | if err != nil { 174 | t.Fatal(err) 175 | } 176 | 177 | sks, pks := conf.conf.KeyGen() 178 | oprfSeed := internal.RandomBytes(conf.conf.Hash.Size()) 179 | 180 | if err = server.SetKeyMaterial(nil, sks, pks, oprfSeed); err != nil { 181 | t.Fatal(err) 182 | } 183 | 184 | rec := buildRecord(credID, oprfSeed, []byte("yo"), pks, client, server) 185 | 186 | ke1 := client.GenerateKE1([]byte("yo")) 187 | ke2, _ := server.GenerateKE2(ke1, rec) 188 | 189 | env, _, err := getEnvelope(client, ke2) 190 | if err != nil { 191 | t.Fatal(err) 192 | } 193 | 194 | // tamper the envelope 195 | env.AuthTag = internal.RandomBytes(client.GetConf().MAC.Size()) 196 | clearText := encoding.Concat(pks, env.Serialize()) 197 | ke2.MaskedResponse = xorResponse(server.GetConf(), rec.MaskingKey, ke2.MaskingNonce, clearText) 198 | 199 | expected := "key recovery: invalid envelope authentication tag" 200 | if _, _, err := client.GenerateKE3(ke2); err == nil || !strings.HasPrefix(err.Error(), expected) { 201 | t.Fatalf("expected error = %q for invalid envelope mac - got %v", expected, err) 202 | } 203 | }) 204 | } 205 | 206 | func cleartextCredentials(clientPublicKey, serverPublicKey, idc, ids []byte) []byte { 207 | if ids == nil { 208 | ids = serverPublicKey 209 | } 210 | 211 | if idc == nil { 212 | idc = clientPublicKey 213 | } 214 | 215 | return encoding.Concat3(serverPublicKey, encoding.EncodeVector(ids), encoding.EncodeVector(idc)) 216 | } 217 | 218 | func TestClientFinish_InvalidKE2KeyEncoding(t *testing.T) { 219 | /* 220 | Tamper KE2 values 221 | */ 222 | credID := internal.RandomBytes(32) 223 | 224 | testAll(t, func(t2 *testing.T, conf *configuration) { 225 | client, err := conf.conf.Client() 226 | if err != nil { 227 | t.Fatal(err) 228 | } 229 | 230 | server, err := conf.conf.Server() 231 | if err != nil { 232 | t.Fatal(err) 233 | } 234 | 235 | sks, pks := conf.conf.KeyGen() 236 | oprfSeed := internal.RandomBytes(conf.conf.Hash.Size()) 237 | 238 | if err := server.SetKeyMaterial(nil, sks, pks, oprfSeed); err != nil { 239 | t.Fatal(err) 240 | } 241 | 242 | rec := buildRecord(credID, oprfSeed, []byte("yo"), pks, client, server) 243 | 244 | ke1 := client.GenerateKE1([]byte("yo")) 245 | ke2, _ := server.GenerateKE2(ke1, rec) 246 | // epks := ke2.ServerPublicKeyshare 247 | 248 | // tamper epks 249 | offset := client.GetConf().Group.ElementLength() + client.GetConf().MAC.Size() 250 | encoded := ke2.Serialize() 251 | badKe2 := encoding.Concat3(encoded[:len(encoded)-offset], getBadElement(t, conf), ke2.ServerMac) 252 | expected := "invalid ephemeral server public key" 253 | if _, err := client.Deserialize.KE2(badKe2); err == nil || !strings.HasPrefix(err.Error(), expected) { 254 | t.Fatalf("expected error for invalid epks encoding - got %q", err) 255 | } 256 | 257 | // tamper PKS 258 | // ke2.ServerPublicKeyshare = server.Group.NewElement().Mult(server.Group.NewScalar().Random()) 259 | env, randomizedPassword, err := getEnvelope(client, ke2) 260 | if err != nil { 261 | t.Fatal(err) 262 | } 263 | 264 | badpks := getBadElement(t, conf) 265 | 266 | ctc := cleartextCredentials( 267 | rec.RegistrationRecord.PublicKey.Encode(), 268 | badpks, 269 | nil, 270 | nil, 271 | ) 272 | authKey := client.GetConf().KDF.Expand( 273 | randomizedPassword, 274 | encoding.SuffixString(env.Nonce, tag.AuthKey), 275 | client.GetConf().KDF.Size(), 276 | ) 277 | authTag := client.GetConf().MAC.MAC(authKey, encoding.Concat(env.Nonce, ctc)) 278 | env.AuthTag = authTag 279 | 280 | clearText := encoding.Concat(badpks, env.Serialize()) 281 | ke2.MaskedResponse = xorResponse(server.GetConf(), rec.MaskingKey, ke2.MaskingNonce, clearText) 282 | 283 | expected = "unmasking: invalid server public key in masked response" 284 | if _, _, err := client.GenerateKE3(ke2); err == nil || !strings.HasPrefix(err.Error(), expected) { 285 | t.Fatalf("expected error %q for invalid envelope mac - got %q", expected, err) 286 | } 287 | 288 | // replace PKS 289 | group := server.GetConf().Group 290 | fakepks := group.Base().Multiply(group.NewScalar().Random()).Encode() 291 | clearText = encoding.Concat(fakepks, env.Serialize()) 292 | ke2.MaskedResponse = xorResponse(server.GetConf(), rec.MaskingKey, ke2.MaskingNonce, clearText) 293 | 294 | expected = "key recovery: invalid envelope authentication tag" 295 | if _, _, err := client.GenerateKE3(ke2); err == nil || !strings.HasPrefix(err.Error(), expected) { 296 | t.Fatalf("expected error %q for invalid envelope mac - got %q", expected, err) 297 | } 298 | }) 299 | } 300 | 301 | func TestClientFinish_InvalidKE2Mac(t *testing.T) { 302 | /* 303 | Invalid server ke2 mac 304 | */ 305 | credID := internal.RandomBytes(32) 306 | 307 | testAll(t, func(t2 *testing.T, conf *configuration) { 308 | client, err := conf.conf.Client() 309 | if err != nil { 310 | t.Fatal(err) 311 | } 312 | 313 | server, err := conf.conf.Server() 314 | if err != nil { 315 | t.Fatal(err) 316 | } 317 | 318 | sks, pks := conf.conf.KeyGen() 319 | oprfSeed := internal.RandomBytes(conf.conf.Hash.Size()) 320 | 321 | if err := server.SetKeyMaterial(nil, sks, pks, oprfSeed); err != nil { 322 | log.Fatal(err) 323 | } 324 | 325 | rec := buildRecord(credID, oprfSeed, []byte("yo"), pks, client, server) 326 | 327 | ke1 := client.GenerateKE1([]byte("yo")) 328 | ke2, _ := server.GenerateKE2(ke1, rec) 329 | 330 | ke2.ServerMac = internal.RandomBytes(client.GetConf().MAC.Size()) 331 | expected := "finalizing AKE: invalid server mac" 332 | if _, _, err := client.GenerateKE3(ke2); err == nil || !strings.HasPrefix(err.Error(), expected) { 333 | t.Fatalf("expected error %q for invalid epks encoding - got %q", expected, err) 334 | } 335 | }) 336 | } 337 | 338 | func TestClientFinish_MissingKe1(t *testing.T) { 339 | expectedError := "missing KE1 in client state" 340 | conf := opaque.DefaultConfiguration() 341 | client, _ := conf.Client() 342 | if _, _, err := client.GenerateKE3(nil); err == nil || !strings.EqualFold(err.Error(), expectedError) { 343 | t.Fatalf( 344 | "expected error when calling GenerateKE3 without pre-existing KE1, want %q, got %q", 345 | expectedError, 346 | err, 347 | ) 348 | } 349 | } 350 | 351 | func TestClientPRK(t *testing.T) { 352 | type prkTest struct { 353 | name string 354 | input string 355 | ksfSalt string 356 | kdfSalt string 357 | output string 358 | ksfParameters []int 359 | ksfLength int 360 | kdf crypto.Hash 361 | ksf ksf.Identifier 362 | } 363 | 364 | tests := []prkTest{ 365 | { 366 | name: "Argon2id", 367 | ksf: ksf.Argon2id, 368 | ksfSalt: "ksfSalt", 369 | ksfLength: 32, 370 | ksfParameters: []int{3, 65536, 4}, 371 | kdf: crypto.SHA512, 372 | kdfSalt: "kdfSalt", 373 | input: "password", 374 | output: "3e858a95d7fe77be3a6278dafa572f8a3a1d49a7154e3a0710d9a5a46358fd0993d958d0963cd88c0a907d105fadcb8c0702b02f8305f8f3c77204b63a93e469", 375 | }, 376 | { 377 | name: "Scrypt", 378 | ksf: ksf.Scrypt, 379 | ksfSalt: "ksfSalt", 380 | ksfLength: 32, 381 | ksfParameters: []int{32768, 8, 1}, 382 | kdf: crypto.SHA512, 383 | kdfSalt: "kdfSalt", 384 | input: "password", 385 | output: "a0d28223edab936f13d778636f1801c0368c2b8d990c5be3cf93d7d1f5ade9d7634a2b20b2f09ac2f1508be6741fcd2f4279ecf33d4b672991b107463016c37f", 386 | }, 387 | { 388 | name: "PBKDF2", 389 | ksf: ksf.PBKDF2Sha512, 390 | ksfSalt: "ksfSalt", 391 | ksfLength: 32, 392 | ksfParameters: []int{10000}, 393 | kdf: crypto.SHA512, 394 | kdfSalt: "kdfSalt", 395 | input: "password", 396 | output: "35bd30915a0564dbd160402bec5163441cc3c8c3c9ee4cf2d87f0f2e228b514cf1c18a41ce9e84b3306286cd06032b296a4a2ff487945e59fcecbab7f06b3098", 397 | }, 398 | { 399 | name: "Identity", 400 | ksf: 0, 401 | ksfSalt: "ksfSalt", 402 | ksfLength: 32, 403 | ksfParameters: []int{1, 2}, 404 | kdf: crypto.SHA512, 405 | kdfSalt: "kdfSalt", 406 | input: "password", 407 | output: "deba3102d5ddf4b833ff43d3d2f3fb77b9514652bb6ce7b985a091478a6c8ecaedb0354d72284202c3de9f358cba8885326403b9738835ae86b6a49fec25ab38", 408 | }, 409 | } 410 | 411 | for _, ksfTest := range tests { 412 | t.Run(ksfTest.name, func(t *testing.T) { 413 | input := []byte(ksfTest.input) 414 | stretcher := internal.NewKSF(ksfTest.ksf) 415 | stretcher.Parameterize(ksfTest.ksfParameters...) 416 | stretched := stretcher.Harden(input, []byte(ksfTest.ksfSalt), ksfTest.ksfLength) 417 | 418 | extract := internal.NewKDF(ksfTest.kdf) 419 | output := hex.EncodeToString(extract.Extract([]byte(ksfTest.kdfSalt), encoding.Concat(input, stretched))) 420 | 421 | if output != ksfTest.output { 422 | t.Errorf("got %q, want %q", output, ksfTest.output) 423 | } 424 | }) 425 | } 426 | } 427 | -------------------------------------------------------------------------------- /tests/deserializer_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright (C) 2020-2025 Daniel Bourdrez. All Rights Reserved. 4 | // 5 | // This source code is licensed under the MIT license found in the 6 | // LICENSE file in the root directory of this source tree or at 7 | // https://spdx.org/licenses/MIT.html 8 | 9 | package opaque_test 10 | 11 | import ( 12 | "encoding/hex" 13 | "errors" 14 | "testing" 15 | 16 | group "github.com/bytemare/ecc" 17 | 18 | "github.com/bytemare/opaque" 19 | "github.com/bytemare/opaque/internal" 20 | "github.com/bytemare/opaque/internal/encoding" 21 | ) 22 | 23 | const testErrValidConf = "unexpected error on valid configuration: %v" 24 | 25 | var errInvalidMessageLength = errors.New("invalid message length for the configuration") 26 | 27 | /* 28 | Message Deserialization 29 | */ 30 | 31 | func TestDeserializer(t *testing.T) { 32 | // Test valid configurations 33 | testAll(t, func(t2 *testing.T, conf *configuration) { 34 | if _, err := conf.conf.Deserializer(); err != nil { 35 | t.Fatalf(testErrValidConf, err) 36 | } 37 | }) 38 | 39 | // Test for an invalid configuration. 40 | conf := &opaque.Configuration{ 41 | OPRF: 0, 42 | AKE: 0, 43 | KSF: 0, 44 | KDF: 0, 45 | MAC: 0, 46 | Hash: 0, 47 | Context: nil, 48 | } 49 | 50 | if _, err := conf.Deserializer(); err == nil { 51 | t.Fatal("expected error on invalid configuration") 52 | } 53 | } 54 | 55 | func TestDeserializeRegistrationRequest(t *testing.T) { 56 | c := opaque.DefaultConfiguration() 57 | 58 | server, _ := c.Server() 59 | conf := server.GetConf() 60 | length := conf.OPRF.Group().ElementLength() + 1 61 | if _, err := server.Deserialize.RegistrationRequest(internal.RandomBytes(length)); err == nil || 62 | err.Error() != errInvalidMessageLength.Error() { 63 | t.Fatalf("Expected error for DeserializeRegistrationRequest. want %q, got %q", errInvalidMessageLength, err) 64 | } 65 | 66 | client, _ := c.Client() 67 | if _, err := client.Deserialize.RegistrationRequest(internal.RandomBytes(length)); err == nil || 68 | err.Error() != errInvalidMessageLength.Error() { 69 | t.Fatalf("Expected error for DeserializeRegistrationRequest. want %q, got %q", errInvalidMessageLength, err) 70 | } 71 | } 72 | 73 | func TestDeserializeRegistrationResponse(t *testing.T) { 74 | c := opaque.DefaultConfiguration() 75 | 76 | server, _ := c.Server() 77 | conf := server.GetConf() 78 | length := conf.OPRF.Group().ElementLength() + conf.Group.ElementLength() + 1 79 | if _, err := server.Deserialize.RegistrationResponse(internal.RandomBytes(length)); err == nil || 80 | err.Error() != errInvalidMessageLength.Error() { 81 | t.Fatalf("Expected error for DeserializeRegistrationRequest. want %q, got %q", errInvalidMessageLength, err) 82 | } 83 | 84 | client, _ := c.Client() 85 | if _, err := client.Deserialize.RegistrationResponse(internal.RandomBytes(length)); err == nil || 86 | err.Error() != errInvalidMessageLength.Error() { 87 | t.Fatalf("Expected error for DeserializeRegistrationRequest. want %q, got %q", errInvalidMessageLength, err) 88 | } 89 | } 90 | 91 | func TestDeserializeRegistrationRecord(t *testing.T) { 92 | testAll(t, func(t2 *testing.T, conf *configuration) { 93 | server, err := conf.conf.Server() 94 | if err != nil { 95 | t.Fatal(err) 96 | } 97 | c := server.GetConf() 98 | length := c.Group.ElementLength() + c.Hash.Size() + c.EnvelopeSize + 1 99 | if _, err := server.Deserialize.RegistrationRecord(internal.RandomBytes(length)); err == nil || 100 | err.Error() != errInvalidMessageLength.Error() { 101 | t.Fatalf("Expected error for DeserializeRegistrationRequest. want %q, got %q", errInvalidMessageLength, err) 102 | } 103 | 104 | badPKu := getBadElement(t, conf) 105 | rec := encoding.Concat(badPKu, internal.RandomBytes(c.Hash.Size()+c.EnvelopeSize)) 106 | 107 | expect := "invalid client public key" 108 | if _, err := server.Deserialize.RegistrationRecord(rec); err == nil || err.Error() != expect { 109 | t.Fatalf("Expected error for DeserializeRegistrationRequest. want %q, got %q", expect, err) 110 | } 111 | 112 | client, err := conf.conf.Client() 113 | if err != nil { 114 | t.Fatal(err) 115 | } 116 | if _, err := client.Deserialize.RegistrationRecord(internal.RandomBytes(length)); err == nil || 117 | err.Error() != errInvalidMessageLength.Error() { 118 | t.Fatalf("Expected error for DeserializeRegistrationRequest. want %q, got %q", errInvalidMessageLength, err) 119 | } 120 | }) 121 | } 122 | 123 | func TestDeserializeKE1(t *testing.T) { 124 | c := opaque.DefaultConfiguration() 125 | g := group.Group(c.AKE) 126 | ke1Length := g.ElementLength() + internal.NonceLength + g.ElementLength() 127 | 128 | server, _ := c.Server() 129 | if _, err := server.Deserialize.KE1(internal.RandomBytes(ke1Length + 1)); err == nil || 130 | err.Error() != errInvalidMessageLength.Error() { 131 | t.Fatalf("Expected error for DeserializeKE1. want %q, got %q", errInvalidMessageLength, err) 132 | } 133 | 134 | client, _ := c.Client() 135 | if _, err := client.Deserialize.KE1(internal.RandomBytes(ke1Length + 1)); err == nil || 136 | err.Error() != errInvalidMessageLength.Error() { 137 | t.Fatalf("Expected error for DeserializeKE1. want %q, got %q", errInvalidMessageLength, err) 138 | } 139 | } 140 | 141 | func TestDeserializeKE2(t *testing.T) { 142 | c := opaque.DefaultConfiguration() 143 | 144 | client, _ := c.Client() 145 | conf := client.GetConf() 146 | ke2Length := conf.OPRF.Group(). 147 | ElementLength() + 148 | 2*conf.NonceLen + 2*conf.Group.ElementLength() + conf.EnvelopeSize + conf.MAC.Size() 149 | if _, err := client.Deserialize.KE2(internal.RandomBytes(ke2Length + 1)); err == nil || 150 | err.Error() != errInvalidMessageLength.Error() { 151 | t.Fatalf("Expected error for DeserializeKE1. want %q, got %q", errInvalidMessageLength, err) 152 | } 153 | 154 | server, _ := c.Server() 155 | conf = server.GetConf() 156 | ke2Length = conf.OPRF.Group(). 157 | ElementLength() + 158 | 2*conf.NonceLen + 2*conf.Group.ElementLength() + conf.EnvelopeSize + conf.MAC.Size() 159 | if _, err := server.Deserialize.KE2(internal.RandomBytes(ke2Length + 1)); err == nil || 160 | err.Error() != errInvalidMessageLength.Error() { 161 | t.Fatalf("Expected error for DeserializeKE1. want %q, got %q", errInvalidMessageLength, err) 162 | } 163 | } 164 | 165 | func TestDeserializeKE3(t *testing.T) { 166 | c := opaque.DefaultConfiguration() 167 | ke3Length := c.MAC.Size() 168 | 169 | server, _ := c.Server() 170 | if _, err := server.Deserialize.KE3(internal.RandomBytes(ke3Length + 1)); err == nil || 171 | err.Error() != errInvalidMessageLength.Error() { 172 | t.Fatalf("Expected error for DeserializeKE1. want %q, got %q", errInvalidMessageLength, err) 173 | } 174 | 175 | client, _ := c.Client() 176 | if _, err := client.Deserialize.KE3(internal.RandomBytes(ke3Length + 1)); err == nil || 177 | err.Error() != errInvalidMessageLength.Error() { 178 | t.Fatalf("Expected error for DeserializeKE1. want %q, got %q", errInvalidMessageLength, err) 179 | } 180 | } 181 | 182 | func TestDecodeAkePrivateKey(t *testing.T) { 183 | testAll(t, func(t2 *testing.T, conf *configuration) { 184 | key := conf.conf.AKE.Group().NewScalar().Random() 185 | 186 | des, err := conf.conf.Deserializer() 187 | if err != nil { 188 | t.Fatalf(testErrValidConf, err) 189 | } 190 | 191 | if _, err = des.DecodeAkePrivateKey(key.Encode()); err != nil { 192 | t.Fatalf("unexpect error on valid private key. Group %v, key %v", 193 | conf.conf.AKE, 194 | key.Hex(), 195 | ) 196 | } 197 | }) 198 | } 199 | 200 | func TestDecodeBadAkePrivateKey(t *testing.T) { 201 | testAll(t, func(t2 *testing.T, conf *configuration) { 202 | badKey := getBadScalar(t, conf) 203 | 204 | des, err := conf.conf.Deserializer() 205 | if err != nil { 206 | t.Fatalf(testErrValidConf, err) 207 | } 208 | 209 | if _, err := des.DecodeAkePrivateKey(badKey); err == nil { 210 | t.Fatalf("expect error on invalid private key. Group %v, key %v", 211 | conf.conf.AKE, 212 | hex.EncodeToString(badKey), 213 | ) 214 | } 215 | }) 216 | } 217 | 218 | func TestDecodeAkePublicKey(t *testing.T) { 219 | testAll(t, func(t2 *testing.T, conf *configuration) { 220 | badKey := getBadElement(t, conf) 221 | 222 | des, err := conf.conf.Deserializer() 223 | if err != nil { 224 | t.Fatalf(testErrValidConf, err) 225 | } 226 | 227 | if _, err := des.DecodeAkePublicKey(badKey); err == nil { 228 | t.Fatalf("expect error on invalid public key. Group %v, key %v", 229 | conf.conf.AKE, 230 | hex.EncodeToString(badKey), 231 | ) 232 | } 233 | }) 234 | } 235 | -------------------------------------------------------------------------------- /tests/encoding_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright (C) 2020-2025 Daniel Bourdrez. All Rights Reserved. 4 | // 5 | // This source code is licensed under the MIT license found in the 6 | // LICENSE file in the root directory of this source tree or at 7 | // https://spdx.org/licenses/MIT.html 8 | 9 | package opaque_test 10 | 11 | import ( 12 | "bytes" 13 | "encoding/hex" 14 | "fmt" 15 | "testing" 16 | 17 | "github.com/bytemare/opaque/internal/encoding" 18 | ) 19 | 20 | func TestEncodeVectorLenPanic(t *testing.T) { 21 | /* 22 | EncodeVectorLen with size > 2 23 | */ 24 | defer func() { 25 | recover() 26 | }() 27 | 28 | encoding.EncodeVectorLen(nil, 5) 29 | t.Fatal("no panic with exceeding encoding length") 30 | } 31 | 32 | func TestDecodeVector(t *testing.T) { 33 | /* 34 | DecodeVector with invalid header and payload 35 | */ 36 | 37 | badHeader := []byte{0} 38 | if _, _, err := encoding.DecodeVector(badHeader); err == nil || 39 | err.Error() != "insufficient header length for decoding" { 40 | t.Fatalf("expected error for short input. Got %q", err) 41 | } 42 | 43 | badPayload := []byte{0, 3, 0, 0} 44 | if _, _, err := encoding.DecodeVector(badPayload); err == nil || 45 | err.Error() != "insufficient total length for decoding" { 46 | t.Fatalf("expected error for short input. Got %q", err) 47 | } 48 | } 49 | 50 | type i2ospTest struct { 51 | encoded []byte 52 | value int 53 | size uint16 54 | } 55 | 56 | var I2OSPVectors = []i2ospTest{ 57 | { 58 | []byte{0}, 0, 1, 59 | }, 60 | { 61 | []byte{1}, 1, 1, 62 | }, 63 | { 64 | []byte{0xff}, 255, 1, 65 | }, 66 | { 67 | []byte{0x01, 0x00}, 256, 2, 68 | }, 69 | { 70 | []byte{0xff, 0xff}, 65535, 2, 71 | }, 72 | { 73 | []byte{0x01, 0x00, 0x00}, 65536, 3, 74 | }, 75 | { 76 | []byte{0xff, 0xff, 0xff}, 16777215, 3, 77 | }, 78 | { 79 | []byte{0x01, 0x00, 0x00, 0x00}, 16777216, 4, 80 | }, 81 | { 82 | []byte{0xff, 0xff, 0xff, 0xff}, 4294967295, 4, 83 | }, 84 | } 85 | 86 | func TestI2OSP(t *testing.T) { 87 | for i, v := range I2OSPVectors { 88 | t.Run(fmt.Sprintf("%d - %d - %v", v.value, v.size, v.encoded), func(t *testing.T) { 89 | r := encoding.I2OSP(v.value, v.size) 90 | 91 | if !bytes.Equal(r, v.encoded) { 92 | t.Fatalf( 93 | "invalid encoding for %d. Expected '%s', got '%v'", 94 | i, 95 | hex.EncodeToString(v.encoded), 96 | hex.EncodeToString(r), 97 | ) 98 | } 99 | 100 | value := encoding.OS2IP(v.encoded) 101 | if v.value != value { 102 | t.Errorf("invalid decoding for %d. Expected %d, got %d", i, v.value, value) 103 | } 104 | }) 105 | } 106 | 107 | var length uint16 = 0 108 | if hasPanic, err := expectPanic(nil, func() { 109 | _ = encoding.I2OSP(1, length) 110 | }); !hasPanic { 111 | t.Fatalf("expected panic with with 0 length: %v", err) 112 | } 113 | 114 | length = 5 115 | if hasPanic, err := expectPanic(nil, func() { 116 | _ = encoding.I2OSP(1, length) 117 | }); !hasPanic { 118 | t.Fatalf("expected panic with length too big: %v", err) 119 | } 120 | 121 | negative := -1 122 | if hasPanic, err := expectPanic(nil, func() { 123 | _ = encoding.I2OSP(negative, 4) 124 | }); !hasPanic { 125 | t.Fatalf("expected panic with negative input: %v", err) 126 | } 127 | 128 | tooLarge := 1 << 32 129 | length = 1 130 | if hasPanic, err := expectPanic(nil, func() { 131 | _ = encoding.I2OSP(tooLarge, length) 132 | }); !hasPanic { 133 | t.Fatalf("expected panic with exceeding value for the length: %v", err) 134 | } 135 | 136 | lengths := map[int]uint16{ 137 | 100: 1, 138 | 1 << 8: 2, 139 | 1 << 16: 3, 140 | (1 << 32) - 1: 4, 141 | } 142 | 143 | for k, v := range lengths { 144 | r := encoding.I2OSP(k, v) 145 | 146 | if uint16(len(r)) != v { 147 | t.Fatalf("invalid length for %d. Expected '%d', got '%d' (%v)", k, v, len(r), r) 148 | } 149 | } 150 | } 151 | 152 | func TestOS2IP(t *testing.T) { 153 | // No input 154 | if hasPanic, _ := expectPanic(nil, func() { 155 | _ = encoding.OS2IP(nil) 156 | }); !hasPanic { 157 | t.Fatal("expected panic with nil input") 158 | } 159 | 160 | // Empty input 161 | if hasPanic, _ := expectPanic(nil, func() { 162 | _ = encoding.OS2IP([]byte("")) 163 | }); !hasPanic { 164 | t.Fatal("expected panic with empty input") 165 | } 166 | 167 | // Exceeding input 168 | input := "12345" 169 | if hasPanic, _ := expectPanic(nil, func() { 170 | _ = encoding.OS2IP([]byte(input)) 171 | }); !hasPanic { 172 | t.Fatal("expected panic with big input") 173 | } 174 | } 175 | 176 | func hasPanic(f func()) (has bool, err error) { 177 | err = nil 178 | var report interface{} 179 | func() { 180 | defer func() { 181 | if report = recover(); report != nil { 182 | has = true 183 | } 184 | }() 185 | 186 | f() 187 | }() 188 | 189 | if has { 190 | err = fmt.Errorf("%v", report) 191 | } 192 | 193 | return 194 | } 195 | 196 | func expectPanic(expectedError error, f func()) (bool, string) { 197 | hasPanic, err := hasPanic(f) 198 | 199 | if !hasPanic { 200 | return false, "no panic" 201 | } 202 | 203 | if expectedError == nil { 204 | return true, "" 205 | } 206 | 207 | if err == nil { 208 | return false, "panic but no message" 209 | } 210 | 211 | if err.Error() != expectedError.Error() { 212 | return false, fmt.Sprintf("expected %q, got %q", expectedError, err) 213 | } 214 | 215 | return true, "" 216 | } 217 | -------------------------------------------------------------------------------- /tests/fuzz_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright (C) 2020-2025 Daniel Bourdrez. All Rights Reserved. 4 | // 5 | // This source code is licensed under the MIT license found in the 6 | // LICENSE file in the root directory of this source tree or at 7 | // https://spdx.org/licenses/MIT.html 8 | 9 | package opaque_test 10 | 11 | import ( 12 | "crypto" 13 | "encoding/json" 14 | "errors" 15 | "fmt" 16 | "log" 17 | "os" 18 | "strings" 19 | "testing" 20 | 21 | "github.com/bytemare/hash" 22 | "github.com/bytemare/ksf" 23 | 24 | "github.com/bytemare/opaque" 25 | "github.com/bytemare/opaque/internal" 26 | "github.com/bytemare/opaque/internal/oprf" 27 | ) 28 | 29 | const fmtGotValidInput = "got %q but input is valid" 30 | 31 | type fuzzConfError struct { 32 | error error 33 | value interface{} 34 | isAvailable bool 35 | } 36 | 37 | // skipErrorOnCondition skips the test if we find the expected error in err and cond if false. 38 | func skipErrorOnCondition(t *testing.T, expected error, ce *fuzzConfError) error { 39 | if strings.Contains(expected.Error(), ce.error.Error()) { 40 | if ce.isAvailable { 41 | return fmt.Errorf("got %q but input is valid: %q", ce.error, ce.value) 42 | } 43 | t.Skip() 44 | } 45 | 46 | return nil 47 | } 48 | 49 | func fuzzTestConfigurationError(t *testing.T, c *opaque.Configuration, err error) error { 50 | // Errors tested for 51 | errorTests := []*fuzzConfError{ 52 | {errors.New("invalid KDF id"), c.KDF, hash.Hash(c.KDF).Available()}, 53 | {errors.New("invalid MAC id"), c.MAC, hash.Hash(c.MAC).Available()}, 54 | {errors.New("invalid Hash id"), c.Hash, hash.Hash(c.Hash).Available()}, 55 | {errors.New("invalid KSF id"), c.KSF, c.KSF == 0 && c.KSF.Available()}, 56 | {errors.New("invalid OPRF group id"), c.OPRF, c.OPRF.Available() && c.OPRF.OPRF().Available()}, 57 | {errors.New("invalid AKE group id"), c.AKE, c.AKE.Available() && c.AKE.Group().Available()}, 58 | } 59 | 60 | for _, test := range errorTests { 61 | if e := skipErrorOnCondition(t, err, test); e != nil { 62 | return e 63 | } 64 | } 65 | 66 | return fmt.Errorf("unrecognized error: %w", err) 67 | } 68 | 69 | func fuzzClientConfiguration(t *testing.T, c *opaque.Configuration) (*opaque.Client, error) { 70 | client, err := c.Client() 71 | if err != nil { 72 | if err = fuzzTestConfigurationError(t, c, err); err != nil { 73 | return nil, err 74 | } 75 | } 76 | if client == nil { 77 | t.Fatal("client is nil") 78 | } 79 | 80 | return client, nil 81 | } 82 | 83 | func fuzzServerConfiguration(t *testing.T, c *opaque.Configuration) (*opaque.Server, error) { 84 | server, err := c.Server() 85 | if err != nil { 86 | if err = fuzzTestConfigurationError(t, c, err); err != nil { 87 | return nil, err 88 | } 89 | } 90 | if server == nil { 91 | t.Fatal("server is nil") 92 | } 93 | 94 | return server, nil 95 | } 96 | 97 | func fuzzLoadVectors(path string) ([]*vector, error) { 98 | contents, err := os.ReadFile(path) 99 | if err != nil { 100 | return nil, fmt.Errorf("no vectors to read: %v", err) 101 | } 102 | 103 | var v []*vector 104 | err = json.Unmarshal(contents, &v) 105 | if err != nil { 106 | return nil, fmt.Errorf("no vectors to read: %v", err) 107 | } 108 | 109 | return v, nil 110 | } 111 | 112 | func FuzzConfiguration(f *testing.F) { 113 | // seed corpus 114 | loadVectorSeedCorpus(f, "") 115 | 116 | f.Fuzz(func(t *testing.T, ke1, context []byte, kdf, mac, h uint, o []byte, ksfID, ake byte) { 117 | c := inputToConfig(context, kdf, mac, h, o, ksfID, ake) 118 | 119 | if _, err := fuzzServerConfiguration(t, c); err != nil { 120 | t.Fatal(err) 121 | } 122 | 123 | if _, err := fuzzServerConfiguration(t, c); err != nil { 124 | t.Fatal(err) 125 | } 126 | }) 127 | } 128 | 129 | func loadVectorSeedCorpus(f *testing.F, stage string) { 130 | // seed corpus 131 | vectors, err := fuzzLoadVectors("vectors.json") 132 | if err != nil { 133 | log.Fatal(err) 134 | } 135 | 136 | for _, v := range vectors { 137 | if v.Config.Group == "curve25519" { 138 | continue 139 | } 140 | 141 | var input ByteToHex 142 | switch stage { 143 | case "": 144 | input = nil 145 | case "RegistrationRequest": 146 | input = v.Outputs.RegistrationRequest 147 | case "RegistrationResponse": 148 | input = v.Outputs.RegistrationResponse 149 | case "RegistrationRecord": 150 | input = v.Outputs.RegistrationRecord 151 | case "KE1": 152 | input = v.Outputs.KE1 153 | case "KE2": 154 | input = v.Outputs.KE2 155 | case "KE3": 156 | input = v.Outputs.KE3 157 | default: 158 | panic(nil) 159 | } 160 | 161 | f.Add([]byte(input), 162 | []byte(v.Config.Context), 163 | uint(kdfToHash(v.Config.KDF)), 164 | uint(macToHash(v.Config.MAC)), 165 | uint(hashToHash(v.Config.Hash)), 166 | []byte(v.Config.OPRF), 167 | byte(ksfToKSF(v.Config.KSF)), 168 | byte(groupToGroup(v.Config.Group)), 169 | ) 170 | } 171 | 172 | // previous crashers 173 | f.Add([]byte("0"), []byte(""), uint(7), uint(37), uint(7), []byte{'\x05'}, byte('\x02'), byte('\x05')) 174 | f.Add([]byte("0"), []byte("0"), uint(13), uint(5), uint(5), []byte{'\x03'}, byte('\r'), byte('\x03')) 175 | f.Add([]byte("0"), []byte("0"), uint(13), uint(5), uint(5), []byte{'\a'}, byte('\x04'), byte('\x03')) 176 | f.Add( 177 | []byte("000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"), 178 | []byte("0"), 179 | uint(7), 180 | uint(7), 181 | uint(7), 182 | []byte{'\x01'}, 183 | byte('\x03'), 184 | byte('\x01'), 185 | ) 186 | f.Add( 187 | []byte("00000000000000000000000000000000"), 188 | []byte("0"), 189 | uint(7), 190 | uint(7), 191 | uint(7), 192 | []byte{'\x01'}, 193 | byte('\x03'), 194 | byte('\x06'), 195 | ) 196 | } 197 | 198 | func inputToConfig(context []byte, kdf, mac, h uint, o []byte, ksfID, ake byte) *opaque.Configuration { 199 | return &opaque.Configuration{ 200 | Context: context, 201 | KDF: crypto.Hash(kdf), 202 | MAC: crypto.Hash(mac), 203 | Hash: crypto.Hash(h), 204 | OPRF: oprfToGroup(oprf.Identifier(o)), 205 | KSF: ksf.Identifier(ksfID), 206 | AKE: opaque.Group(ake), 207 | } 208 | } 209 | 210 | func FuzzDeserializeRegistrationRequest(f *testing.F) { 211 | // Errors tested for 212 | var ( 213 | errInvalidMessageLength = errors.New("invalid message length for the configuration") 214 | errInvalidBlindedData = errors.New("blinded data is an invalid point") 215 | ) 216 | 217 | loadVectorSeedCorpus(f, "RegistrationRequest") 218 | 219 | f.Fuzz(func(t *testing.T, r1, context []byte, kdf, mac, h uint, oprf []byte, ksfID, ake byte) { 220 | c := inputToConfig(context, kdf, mac, h, oprf, ksfID, ake) 221 | server, err := c.Server() 222 | if err != nil { 223 | t.Skip() 224 | } 225 | 226 | _, err = server.Deserialize.RegistrationRequest(r1) 227 | if err != nil { 228 | conf := server.GetConf() 229 | if strings.Contains(err.Error(), errInvalidMessageLength.Error()) && 230 | len(r1) == conf.OPRF.Group().ElementLength() { 231 | t.Fatalf("got %q but input length is valid", errInvalidMessageLength) 232 | } 233 | 234 | if strings.Contains(err.Error(), errInvalidBlindedData.Error()) { 235 | if err := isValidOPRFPoint(conf, r1[:conf.OPRF.Group().ElementLength()], errInvalidBlindedData); err != nil { 236 | t.Fatal(err) 237 | } 238 | } 239 | } 240 | }) 241 | } 242 | 243 | func FuzzDeserializeRegistrationResponse(f *testing.F) { 244 | // Errors tested for 245 | var ( 246 | errInvalidMessageLength = errors.New("invalid message length for the configuration") 247 | errInvalidEvaluatedData = errors.New("invalid OPRF evaluation") 248 | errInvalidServerPK = errors.New("invalid server public key") 249 | ) 250 | 251 | loadVectorSeedCorpus(f, "RegistrationResponse") 252 | 253 | f.Fuzz(func(t *testing.T, r2, context []byte, kdf, mac, h uint, oprf []byte, ksfID, ake byte) { 254 | c := inputToConfig(context, kdf, mac, h, oprf, ksfID, ake) 255 | client, err := c.Client() 256 | if err != nil { 257 | t.Skip() 258 | } 259 | 260 | _, err = client.Deserialize.RegistrationResponse(r2) 261 | if err != nil { 262 | conf := client.GetConf() 263 | maxResponseLength := conf.OPRF.Group().ElementLength() + conf.Group.ElementLength() 264 | 265 | if strings.Contains(err.Error(), errInvalidMessageLength.Error()) && len(r2) == maxResponseLength { 266 | t.Fatalf(fmtGotValidInput, errInvalidMessageLength) 267 | } 268 | 269 | if strings.Contains(err.Error(), errInvalidEvaluatedData.Error()) { 270 | if err := isValidOPRFPoint(conf, r2[:conf.OPRF.Group().ElementLength()], errInvalidEvaluatedData); err != nil { 271 | t.Fatal(err) 272 | } 273 | } 274 | 275 | if strings.Contains(err.Error(), errInvalidServerPK.Error()) { 276 | if err := isValidAKEPoint(conf, r2[conf.OPRF.Group().ElementLength():], errInvalidServerPK); err != nil { 277 | t.Fatal(err) 278 | } 279 | } 280 | } 281 | }) 282 | } 283 | 284 | func FuzzDeserializeRegistrationRecord(f *testing.F) { 285 | // Errors tested for 286 | var ( 287 | errInvalidMessageLength = errors.New("invalid message length for the configuration") 288 | errInvalidClientPK = errors.New("invalid client public key") 289 | ) 290 | 291 | loadVectorSeedCorpus(f, "RegistrationRecord") 292 | 293 | f.Fuzz(func(t *testing.T, r3, context []byte, kdf, mac, h uint, oprf []byte, ksfID, ake byte) { 294 | c := inputToConfig(context, kdf, mac, h, oprf, ksfID, ake) 295 | server, err := c.Server() 296 | if err != nil { 297 | t.Skip() 298 | } 299 | 300 | conf := server.GetConf() 301 | 302 | _, err = server.Deserialize.RegistrationRecord(r3) 303 | if err != nil { 304 | maxMessageLength := conf.Group.ElementLength() + conf.Hash.Size() + conf.EnvelopeSize 305 | 306 | if strings.Contains(err.Error(), errInvalidMessageLength.Error()) && len(r3) == maxMessageLength { 307 | t.Fatalf(fmtGotValidInput, errInvalidMessageLength) 308 | } 309 | 310 | if strings.Contains(err.Error(), errInvalidClientPK.Error()) { 311 | if err := isValidAKEPoint(conf, r3[:conf.Group.ElementLength()], errInvalidClientPK); err != nil { 312 | t.Fatal(err) 313 | } 314 | } 315 | } 316 | }) 317 | } 318 | 319 | func FuzzDeserializeKE1(f *testing.F) { 320 | // Errors tested for 321 | var ( 322 | errInvalidMessageLength = errors.New("invalid message length for the configuration") 323 | errInvalidBlindedData = errors.New("blinded data is an invalid point") 324 | errInvalidClientEPK = errors.New("invalid ephemeral client public key") 325 | ) 326 | 327 | loadVectorSeedCorpus(f, "KE1") 328 | 329 | f.Fuzz(func(t *testing.T, ke1, context []byte, kdf, mac, h uint, oprf []byte, ksfID, ake byte) { 330 | c := inputToConfig(context, kdf, mac, h, oprf, ksfID, ake) 331 | server, err := c.Server() 332 | if err != nil { 333 | t.Skip() 334 | } 335 | 336 | _, err = server.Deserialize.KE1(ke1) 337 | if err != nil { 338 | conf := server.GetConf() 339 | if strings.Contains(err.Error(), errInvalidMessageLength.Error()) && 340 | len(ke1) == conf.OPRF.Group().ElementLength()+conf.NonceLen+conf.Group.ElementLength() { 341 | t.Fatalf("got %q but input length is valid", errInvalidMessageLength) 342 | } 343 | 344 | if strings.Contains(err.Error(), errInvalidBlindedData.Error()) { 345 | input := ke1[:conf.OPRF.Group().ElementLength()] 346 | if err := isValidOPRFPoint(conf, input, errInvalidBlindedData); err != nil { 347 | t.Fatal(err) 348 | } 349 | } 350 | 351 | if strings.Contains(err.Error(), errInvalidClientEPK.Error()) { 352 | input := ke1[conf.OPRF.Group().ElementLength()+conf.NonceLen:] 353 | if err := isValidOPRFPoint(conf, input, errInvalidClientEPK); err != nil { 354 | t.Fatal(err) 355 | } 356 | } 357 | } 358 | }) 359 | } 360 | 361 | func isValidAKEPoint(conf *internal.Configuration, input []byte, err error) error { 362 | e := conf.Group.NewElement() 363 | if err2 := e.Decode(input); err2 == nil { 364 | if e.IsIdentity() { 365 | return errors.New("point is identity/infinity") 366 | } 367 | 368 | return fmt.Errorf("got %q but point is valid", err) 369 | } 370 | 371 | return nil 372 | } 373 | 374 | func isValidOPRFPoint(conf *internal.Configuration, input []byte, err error) error { 375 | e := conf.OPRF.Group().NewElement() 376 | if err2 := e.Decode(input); err2 == nil { 377 | if e.IsIdentity() { 378 | return errors.New("point is identity/infinity") 379 | } 380 | 381 | return fmt.Errorf("got %q but point is valid", err) 382 | } 383 | 384 | return nil 385 | } 386 | 387 | func FuzzDeserializeKE2(f *testing.F) { 388 | // Errors tested for 389 | var ( 390 | errInvalidMessageLength = errors.New("invalid message length for the configuration") 391 | errInvalidEvaluatedData = errors.New("invalid OPRF evaluation") 392 | errInvalidServerEPK = errors.New("invalid ephemeral server public key") 393 | ) 394 | 395 | loadVectorSeedCorpus(f, "KE2") 396 | 397 | f.Fuzz(func(t *testing.T, ke2, context []byte, kdf, mac, h uint, oprf []byte, ksfID, ake byte) { 398 | c := inputToConfig(context, kdf, mac, h, oprf, ksfID, ake) 399 | client, err := c.Client() 400 | if err != nil { 401 | t.Skip() 402 | } 403 | 404 | _, err = client.Deserialize.KE2(ke2) 405 | if err != nil { 406 | conf := client.GetConf() 407 | maxResponseLength := conf.OPRF.Group(). 408 | ElementLength() + 409 | conf.NonceLen + conf.Group.ElementLength() + conf.EnvelopeSize 410 | 411 | if strings.Contains(err.Error(), errInvalidMessageLength.Error()) && 412 | len(ke2) == maxResponseLength+conf.NonceLen+conf.Group.ElementLength()+conf.MAC.Size() { 413 | t.Fatalf(fmtGotValidInput, errInvalidMessageLength) 414 | } 415 | 416 | if strings.Contains(err.Error(), errInvalidEvaluatedData.Error()) { 417 | input := ke2[:conf.OPRF.Group().ElementLength()] 418 | if err := isValidOPRFPoint(conf, input, errInvalidEvaluatedData); err != nil { 419 | t.Fatal(err) 420 | } 421 | } 422 | 423 | if strings.Contains(err.Error(), errInvalidServerEPK.Error()) { 424 | input := ke2[conf.OPRF.Group().ElementLength()+conf.NonceLen:] 425 | if err := isValidAKEPoint(conf, input, errInvalidServerEPK); err != nil { 426 | t.Fatal(err) 427 | } 428 | } 429 | } 430 | }) 431 | } 432 | 433 | func FuzzDeserializeKE3(f *testing.F) { 434 | // Error tested for 435 | errInvalidMessageLength := errors.New("invalid message length for the configuration") 436 | 437 | loadVectorSeedCorpus(f, "KE3") 438 | 439 | f.Fuzz(func(t *testing.T, ke3, context []byte, kdf, mac, h uint, oprf []byte, ksfID, ake byte) { 440 | c := inputToConfig(context, kdf, mac, h, oprf, ksfID, ake) 441 | server, err := c.Server() 442 | if err != nil { 443 | t.Skip() 444 | } 445 | 446 | _, err = server.Deserialize.KE3(ke3) 447 | if err != nil { 448 | conf := server.GetConf() 449 | maxMessageLength := conf.MAC.Size() 450 | 451 | if strings.Contains(err.Error(), errInvalidMessageLength.Error()) && len(ke3) == maxMessageLength { 452 | t.Fatalf(fmtGotValidInput, errInvalidMessageLength) 453 | } 454 | } 455 | }) 456 | } 457 | -------------------------------------------------------------------------------- /tests/helper_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright (C) 2020-2025 Daniel Bourdrez. All Rights Reserved. 4 | // 5 | // This source code is licensed under the MIT license found in the 6 | // LICENSE file in the root directory of this source tree or at 7 | // https://spdx.org/licenses/MIT.html 8 | 9 | package opaque_test 10 | 11 | import ( 12 | "crypto" 13 | "crypto/elliptic" 14 | "encoding/hex" 15 | "fmt" 16 | "log" 17 | "math/big" 18 | "testing" 19 | 20 | group "github.com/bytemare/ecc" 21 | "github.com/bytemare/ksf" 22 | 23 | "github.com/bytemare/opaque" 24 | "github.com/bytemare/opaque/internal" 25 | "github.com/bytemare/opaque/internal/encoding" 26 | "github.com/bytemare/opaque/internal/keyrecovery" 27 | "github.com/bytemare/opaque/internal/oprf" 28 | "github.com/bytemare/opaque/internal/tag" 29 | "github.com/bytemare/opaque/message" 30 | ) 31 | 32 | func init() { 33 | log.SetFlags(log.LstdFlags | log.Lshortfile) 34 | } 35 | 36 | // helper functions 37 | 38 | type configuration struct { 39 | curve elliptic.Curve 40 | conf *opaque.Configuration 41 | name string 42 | } 43 | 44 | var configurationTable = []configuration{ 45 | { 46 | name: "Ristretto255", 47 | conf: opaque.DefaultConfiguration(), 48 | curve: nil, 49 | }, 50 | { 51 | name: "P256Sha256", 52 | conf: &opaque.Configuration{ 53 | OPRF: opaque.P256Sha256, 54 | KDF: crypto.SHA256, 55 | MAC: crypto.SHA256, 56 | Hash: crypto.SHA256, 57 | KSF: ksf.Argon2id, 58 | AKE: opaque.P256Sha256, 59 | }, 60 | curve: elliptic.P256(), 61 | }, 62 | { 63 | name: "P384Sha512", 64 | conf: &opaque.Configuration{ 65 | OPRF: opaque.P384Sha512, 66 | KDF: crypto.SHA512, 67 | MAC: crypto.SHA512, 68 | Hash: crypto.SHA512, 69 | KSF: ksf.Argon2id, 70 | AKE: opaque.P384Sha512, 71 | }, 72 | curve: elliptic.P384(), 73 | }, 74 | { 75 | name: "P521Sha512", 76 | conf: &opaque.Configuration{ 77 | OPRF: opaque.P521Sha512, 78 | KDF: crypto.SHA512, 79 | MAC: crypto.SHA512, 80 | Hash: crypto.SHA512, 81 | KSF: ksf.Argon2id, 82 | AKE: opaque.P521Sha512, 83 | }, 84 | curve: elliptic.P521(), 85 | }, 86 | } 87 | 88 | func testAll(t *testing.T, f func(*testing.T, *configuration)) { 89 | for _, test := range configurationTable { 90 | t.Run(test.name, func(t *testing.T) { 91 | f(t, &test) 92 | }) 93 | } 94 | } 95 | 96 | func getBadRistrettoScalar() []byte { 97 | a := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" 98 | decoded, _ := hex.DecodeString(a) 99 | 100 | return decoded 101 | } 102 | 103 | func getBadRistrettoElement() []byte { 104 | a := "2a292df7e32cababbd9de088d1d1abec9fc0440f637ed2fba145094dc14bea08" 105 | decoded, _ := hex.DecodeString(a) 106 | 107 | return decoded 108 | } 109 | 110 | func getBad25519Element() []byte { 111 | a := "efffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f" 112 | decoded, _ := hex.DecodeString(a) 113 | 114 | return decoded 115 | } 116 | 117 | func getBad25519Scalar() []byte { 118 | a := "ecd3f55c1a631258d69cf7a2def9de1400000000000000000000000000000011" 119 | decoded, _ := hex.DecodeString(a) 120 | 121 | return decoded 122 | } 123 | 124 | func badScalar(t *testing.T, g group.Group, curve elliptic.Curve) []byte { 125 | order := curve.Params().P 126 | exceeded := new(big.Int).Add(order, big.NewInt(2)).Bytes() 127 | 128 | err := g.NewScalar().Decode(exceeded) 129 | if err == nil { 130 | t.Errorf("Exceeding order did not yield an error for group %s", g) 131 | } 132 | 133 | return exceeded 134 | } 135 | 136 | func getBadNistElement(t *testing.T, id group.Group) []byte { 137 | size := id.ElementLength() 138 | element := internal.RandomBytes(size) 139 | // detag compression 140 | element[0] = 4 141 | 142 | // test if invalid compression is detected 143 | err := id.NewElement().Decode(element) 144 | if err == nil { 145 | t.Errorf("detagged compressed point did not yield an error for group %s", id) 146 | } 147 | 148 | return element 149 | } 150 | 151 | func getBadElement(t *testing.T, c *configuration) []byte { 152 | switch c.conf.AKE { 153 | case opaque.RistrettoSha512: 154 | return getBadRistrettoElement() 155 | default: 156 | return getBadNistElement(t, group.Group(c.conf.AKE)) 157 | } 158 | } 159 | 160 | func getBadScalar(t *testing.T, c *configuration) []byte { 161 | switch c.conf.AKE { 162 | case opaque.RistrettoSha512: 163 | return getBadRistrettoScalar() 164 | default: 165 | return badScalar(t, oprf.IDFromGroup(group.Group(c.conf.AKE)).Group(), c.curve) 166 | } 167 | } 168 | 169 | func buildRecord( 170 | credID, oprfSeed, password, pks []byte, 171 | client *opaque.Client, 172 | server *opaque.Server, 173 | ) *opaque.ClientRecord { 174 | conf := server.GetConf() 175 | r1 := client.RegistrationInit(password) 176 | 177 | pk := conf.Group.NewElement() 178 | if err := pk.Decode(pks); err != nil { 179 | panic(err) 180 | } 181 | 182 | r2 := server.RegistrationResponse(r1, pk, credID, oprfSeed) 183 | r3, _ := client.RegistrationFinalize(r2) 184 | 185 | return &opaque.ClientRecord{ 186 | CredentialIdentifier: credID, 187 | ClientIdentity: nil, 188 | RegistrationRecord: r3, 189 | } 190 | } 191 | 192 | func xorResponse(c *internal.Configuration, key, nonce, in []byte) []byte { 193 | pad := c.KDF.Expand( 194 | key, 195 | encoding.SuffixString(nonce, tag.CredentialResponsePad), 196 | c.Group.ElementLength()+c.EnvelopeSize, 197 | ) 198 | 199 | dst := make([]byte, len(pad)) 200 | 201 | // if the size is fixed, we could unroll the loop 202 | for i, r := range pad { 203 | dst[i] = r ^ in[i] 204 | } 205 | 206 | return dst 207 | } 208 | 209 | func buildPRK(client *opaque.Client, evaluation *group.Element) ([]byte, error) { 210 | conf := client.GetConf() 211 | unblinded := client.OPRF.Finalize(evaluation) 212 | hardened := conf.KSF.Harden(unblinded, nil, conf.OPRF.Group().ElementLength()) 213 | 214 | return conf.KDF.Extract(nil, encoding.Concat(unblinded, hardened)), nil 215 | } 216 | 217 | func getEnvelope(client *opaque.Client, ke2 *message.KE2) (*keyrecovery.Envelope, []byte, error) { 218 | conf := client.GetConf() 219 | 220 | randomizedPassword, err := buildPRK(client, ke2.EvaluatedMessage) 221 | if err != nil { 222 | return nil, nil, fmt.Errorf("finalizing OPRF : %w", err) 223 | } 224 | 225 | maskingKey := conf.KDF.Expand(randomizedPassword, []byte(tag.MaskingKey), conf.KDF.Size()) 226 | clearText := xorResponse(conf, maskingKey, ke2.MaskingNonce, ke2.MaskedResponse) 227 | e := clearText[conf.Group.ElementLength():] 228 | 229 | env := &keyrecovery.Envelope{ 230 | Nonce: e[:conf.NonceLen], 231 | AuthTag: e[conf.NonceLen:], 232 | } 233 | 234 | return env, randomizedPassword, nil 235 | } 236 | -------------------------------------------------------------------------------- /tests/opaque_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright (C) 2020-2025 Daniel Bourdrez. All Rights Reserved. 4 | // 5 | // This source code is licensed under the MIT license found in the 6 | // LICENSE file in the root directory of this source tree or at 7 | // https://spdx.org/licenses/MIT.html 8 | 9 | package opaque_test 10 | 11 | import ( 12 | "bytes" 13 | "crypto" 14 | "errors" 15 | "reflect" 16 | "strings" 17 | "testing" 18 | 19 | group "github.com/bytemare/ecc" 20 | "github.com/bytemare/ksf" 21 | 22 | "github.com/bytemare/opaque" 23 | "github.com/bytemare/opaque/internal" 24 | "github.com/bytemare/opaque/internal/oprf" 25 | ) 26 | 27 | const dbgErr = "%v" 28 | 29 | type testParams struct { 30 | *opaque.Configuration 31 | username, userID, serverID, password, serverSecretKey, serverPublicKey, oprfSeed, ksfSalt, kdfSalt []byte 32 | ksfParameters []int 33 | ksfLength, nonceLength uint32 34 | } 35 | 36 | func TestFull(t *testing.T) { 37 | ids := []byte("server") 38 | username := []byte("client") 39 | password := []byte("password") 40 | 41 | conf := opaque.DefaultConfiguration() 42 | conf.Context = []byte("OPAQUETest") 43 | conf.KSF = ksf.Argon2id 44 | 45 | tester := &testParams{ 46 | Configuration: conf, 47 | username: username, 48 | userID: username, 49 | serverID: ids, 50 | password: password, 51 | oprfSeed: conf.GenerateOPRFSeed(), 52 | ksfParameters: []int{3, 65536, 4}, 53 | ksfSalt: []byte("ksfSalt"), 54 | kdfSalt: []byte("kdfSalt"), 55 | nonceLength: internal.NonceLength, 56 | } 57 | 58 | serverSecretKey, pks := conf.KeyGen() 59 | tester.serverSecretKey = serverSecretKey 60 | tester.serverPublicKey = pks 61 | 62 | /* 63 | Registration 64 | */ 65 | _, _, record, exportKeyReg := testRegistration(t, tester) 66 | 67 | /* 68 | Login 69 | */ 70 | _, _, exportKeyLogin := testAuthentication(t, tester, record) 71 | 72 | // Check values 73 | if !bytes.Equal(exportKeyReg, exportKeyLogin) { 74 | t.Errorf("export keys differ") 75 | } 76 | } 77 | 78 | func testRegistration(t *testing.T, p *testParams) (*opaque.Client, *opaque.Server, *opaque.ClientRecord, []byte) { 79 | // Client 80 | client, _ := p.Client() 81 | 82 | var m1s []byte 83 | { 84 | reqReg := client.RegistrationInit(p.password) 85 | m1s = reqReg.Serialize() 86 | } 87 | 88 | // Server 89 | var m2s []byte 90 | var credID []byte 91 | { 92 | server, _ := p.Server() 93 | m1, err := server.Deserialize.RegistrationRequest(m1s) 94 | if err != nil { 95 | t.Fatalf(dbgErr, err) 96 | } 97 | 98 | credID = internal.RandomBytes(32) 99 | pks, err := server.Deserialize.DecodeAkePublicKey(p.serverPublicKey) 100 | if err != nil { 101 | t.Fatalf(dbgErr, err) 102 | } 103 | 104 | respReg := server.RegistrationResponse(m1, pks, credID, p.oprfSeed) 105 | 106 | m2s = respReg.Serialize() 107 | } 108 | 109 | // Client 110 | var m3s []byte 111 | var exportKeyReg []byte 112 | { 113 | m2, err := client.Deserialize.RegistrationResponse(m2s) 114 | if err != nil { 115 | t.Fatalf(dbgErr, err) 116 | } 117 | 118 | upload, key := client.RegistrationFinalize(m2, opaque.ClientRegistrationFinalizeOptions{ 119 | ClientIdentity: p.username, 120 | ServerIdentity: p.serverID, 121 | KDFSalt: p.kdfSalt, 122 | KSFSalt: p.ksfSalt, 123 | KSFParameters: p.ksfParameters, 124 | KSFLength: p.ksfLength, 125 | }) 126 | exportKeyReg = key 127 | 128 | m3s = upload.Serialize() 129 | } 130 | 131 | // Server 132 | { 133 | server, _ := p.Server() 134 | m3, err := server.Deserialize.RegistrationRecord(m3s) 135 | if err != nil { 136 | t.Fatalf(dbgErr, err) 137 | } 138 | 139 | return client, server, &opaque.ClientRecord{ 140 | CredentialIdentifier: credID, 141 | ClientIdentity: p.username, 142 | RegistrationRecord: m3, 143 | }, exportKeyReg 144 | } 145 | } 146 | 147 | func testAuthentication( 148 | t *testing.T, 149 | p *testParams, 150 | record *opaque.ClientRecord, 151 | ) (*opaque.Client, *opaque.Server, []byte) { 152 | // Client 153 | client, _ := p.Client() 154 | 155 | var m4s []byte 156 | { 157 | ke1 := client.GenerateKE1(p.password) 158 | m4s = ke1.Serialize() 159 | } 160 | 161 | // Server 162 | var m5s []byte 163 | var state []byte 164 | server, _ := p.Server() 165 | { 166 | if err := server.SetKeyMaterial(p.serverID, p.serverSecretKey, p.serverPublicKey, p.oprfSeed); err != nil { 167 | t.Fatal(err) 168 | } 169 | 170 | m4, err := server.Deserialize.KE1(m4s) 171 | if err != nil { 172 | t.Fatalf(dbgErr, err) 173 | } 174 | 175 | ke2, err := server.GenerateKE2(m4, record) 176 | if err != nil { 177 | t.Fatalf(dbgErr, err) 178 | } 179 | 180 | state = server.SerializeState() 181 | 182 | m5s = ke2.Serialize() 183 | } 184 | 185 | // Client 186 | var m6s []byte 187 | var exportKeyLogin []byte 188 | var clientKey []byte 189 | { 190 | m5, err := client.Deserialize.KE2(m5s) 191 | if err != nil { 192 | t.Fatalf(dbgErr, err) 193 | } 194 | 195 | ke3, key, err := client.GenerateKE3(m5, opaque.GenerateKE3Options{ 196 | ClientIdentity: p.username, 197 | ServerIdentity: p.serverID, 198 | KDFSalt: p.kdfSalt, 199 | KSFSalt: p.ksfSalt, 200 | KSFParameters: p.ksfParameters, 201 | KSFLength: p.ksfLength, 202 | }) 203 | if err != nil { 204 | t.Fatalf(dbgErr, err) 205 | } 206 | exportKeyLogin = key 207 | 208 | m6s = ke3.Serialize() 209 | clientKey = client.SessionKey() 210 | } 211 | 212 | // Server 213 | var serverKey []byte 214 | { 215 | // here we spawn a new server instance to test setting the state 216 | resumedServer, _ := p.Server() 217 | if err := resumedServer.SetAKEState(state); err != nil { 218 | t.Fatalf(dbgErr, err) 219 | } 220 | 221 | m6, err := resumedServer.Deserialize.KE3(m6s) 222 | if err != nil { 223 | t.Fatalf(dbgErr, err) 224 | } 225 | 226 | if err := resumedServer.LoginFinish(m6); err != nil { 227 | t.Fatalf(dbgErr, err) 228 | } 229 | 230 | serverKey = resumedServer.SessionKey() 231 | } 232 | 233 | if !bytes.Equal(clientKey, serverKey) { 234 | t.Fatalf("session keys differ") 235 | } 236 | 237 | return client, server, exportKeyLogin 238 | } 239 | 240 | func isSameConf(a, b *opaque.Configuration) bool { 241 | if a.OPRF != b.OPRF { 242 | return false 243 | } 244 | if a.KDF != b.KDF { 245 | return false 246 | } 247 | if a.MAC != b.MAC { 248 | return false 249 | } 250 | if a.Hash != b.Hash { 251 | return false 252 | } 253 | if !reflect.DeepEqual(a.KSF, b.KSF) { 254 | return false 255 | } 256 | if a.AKE != b.AKE { 257 | return false 258 | } 259 | 260 | return bytes.Equal(a.Context, b.Context) 261 | } 262 | 263 | func TestConfiguration_Deserialization(t *testing.T) { 264 | conf := opaque.DefaultConfiguration() 265 | ser := conf.Serialize() 266 | 267 | conf2, err := opaque.DeserializeConfiguration(ser) 268 | if err != nil { 269 | t.Fatalf("unexpected error on valid configuration: %v", err) 270 | } 271 | 272 | if !isSameConf(conf, conf2) { 273 | t.Fatalf("Unexpected inequality:\n\t%v\n\t%v", conf, conf2) 274 | } 275 | } 276 | 277 | func TestFlush(t *testing.T) { 278 | ids := []byte("server") 279 | username := []byte("client") 280 | password := []byte("password") 281 | 282 | conf := opaque.DefaultConfiguration() 283 | conf.Context = []byte("OPAQUETest") 284 | 285 | test := &testParams{ 286 | Configuration: conf, 287 | username: username, 288 | userID: username, 289 | serverID: ids, 290 | password: password, 291 | oprfSeed: conf.GenerateOPRFSeed(), 292 | } 293 | 294 | serverSecretKey, pks := conf.KeyGen() 295 | test.serverSecretKey = serverSecretKey 296 | test.serverPublicKey = pks 297 | 298 | /* 299 | Registration 300 | */ 301 | _, _, record, _ := testRegistration(t, test) 302 | 303 | /* 304 | Login 305 | */ 306 | client, server, _ := testAuthentication(t, test, record) 307 | 308 | client.Ake.Flush() 309 | if client.SessionKey() != nil { 310 | t.Fatalf("client flush failed, the session key is non-nil: %v", client.SessionKey()) 311 | } 312 | 313 | if client.Ake.GetEphemeralSecretKey() != nil { 314 | t.Fatalf("client flush failed, the ephemeral session key is non-nil: %v", client.SessionKey()) 315 | } 316 | 317 | if client.Ake.GetNonce() != nil { 318 | t.Fatalf("client flush failed, the nonce is non-nil: %v", client.SessionKey()) 319 | } 320 | 321 | server.Ake.Flush() 322 | if server.SessionKey() != nil { 323 | t.Fatalf("server flush failed, the session key is non-nil: %v", client.SessionKey()) 324 | } 325 | 326 | if server.Ake.GetEphemeralSecretKey() != nil { 327 | t.Fatalf("server flush failed, the ephemeral session key is non-nil: %v", client.SessionKey()) 328 | } 329 | 330 | if server.Ake.GetNonce() != nil { 331 | t.Fatalf("server flush failed, the nonce is non-nil: %v", client.SessionKey()) 332 | } 333 | 334 | if server.ExpectedMAC() != nil { 335 | t.Fatalf("server flush failed, the expected client mac is non-nil: %v", client.SessionKey()) 336 | } 337 | } 338 | 339 | /* 340 | The following tests look for failing conditions. 341 | */ 342 | 343 | func TestDeserializeConfiguration_InvalidContextHeader(t *testing.T) { 344 | d := opaque.DefaultConfiguration().Serialize() 345 | d[7] = 20 346 | 347 | expected := "decoding the configuration context: " 348 | if _, err := opaque.DeserializeConfiguration(d); err == nil || !strings.HasPrefix(err.Error(), expected) { 349 | t.Errorf( 350 | "DeserializeConfiguration did not return the appropriate error for vector invalid header. want %q, got %q", 351 | expected, 352 | err, 353 | ) 354 | } 355 | } 356 | 357 | func TestNilConfiguration(t *testing.T) { 358 | def := opaque.DefaultConfiguration() 359 | g := group.Group(def.AKE) 360 | defaultConfiguration := &internal.Configuration{ 361 | OPRF: oprf.IDFromGroup(g), 362 | Group: g, 363 | KSF: internal.NewKSF(def.KSF), 364 | KDF: internal.NewKDF(def.KDF), 365 | MAC: internal.NewMac(def.MAC), 366 | Hash: internal.NewHash(def.Hash), 367 | NonceLen: internal.NonceLength, 368 | Context: def.Context, 369 | } 370 | 371 | if s, _ := opaque.NewServer(nil); reflect.DeepEqual(s.GetConf(), defaultConfiguration) { 372 | t.Errorf("server did not default to correct configuration") 373 | } 374 | 375 | if c, _ := opaque.NewClient(nil); reflect.DeepEqual(c.GetConf(), defaultConfiguration) { 376 | t.Errorf("client did not default to correct configuration") 377 | } 378 | } 379 | 380 | func TestDeserializeConfiguration_Short(t *testing.T) { 381 | r9 := internal.RandomBytes(7) 382 | 383 | if _, err := opaque.DeserializeConfiguration(r9); !errors.Is(err, internal.ErrConfigurationInvalidLength) { 384 | t.Errorf("DeserializeConfiguration did not return the appropriate error for vector r9. want %q, got %q", 385 | internal.ErrConfigurationInvalidLength, err) 386 | } 387 | } 388 | 389 | func TestBadConfiguration(t *testing.T) { 390 | setBadValue := func(pos, val int) []byte { 391 | b := opaque.DefaultConfiguration().Serialize() 392 | b[pos] = byte(val) 393 | return b 394 | } 395 | 396 | tests := []struct { 397 | name string 398 | makeBad func() []byte 399 | error string 400 | }{ 401 | { 402 | name: "Bad OPRF", 403 | makeBad: func() []byte { 404 | return setBadValue(0, 0) 405 | }, 406 | error: "invalid OPRF group id", 407 | }, 408 | { 409 | name: "Bad AKE", 410 | makeBad: func() []byte { 411 | return setBadValue(1, 0) 412 | }, 413 | error: "invalid AKE group id", 414 | }, 415 | { 416 | name: "Bad KSF", 417 | makeBad: func() []byte { 418 | return setBadValue(2, 10) 419 | }, 420 | error: "invalid KSF id", 421 | }, 422 | { 423 | name: "Bad KDF", 424 | makeBad: func() []byte { 425 | return setBadValue(3, 0) 426 | }, 427 | error: "invalid KDF id", 428 | }, 429 | { 430 | name: "Bad MAC", 431 | makeBad: func() []byte { 432 | return setBadValue(4, 0) 433 | }, 434 | error: "invalid MAC id", 435 | }, 436 | { 437 | name: "Bad Hash", 438 | makeBad: func() []byte { 439 | return setBadValue(5, 0) 440 | }, 441 | error: "invalid Hash id", 442 | }, 443 | } 444 | 445 | convertToBadConf := func(encoded []byte) *opaque.Configuration { 446 | return &opaque.Configuration{ 447 | OPRF: opaque.Group(encoded[0]), 448 | AKE: opaque.Group(encoded[1]), 449 | KSF: ksf.Identifier(encoded[2]), 450 | KDF: crypto.Hash(encoded[3]), 451 | MAC: crypto.Hash(encoded[4]), 452 | Hash: crypto.Hash(encoded[5]), 453 | Context: encoded[6:], 454 | } 455 | } 456 | 457 | for _, badConf := range tests { 458 | t.Run(badConf.name, func(t *testing.T) { 459 | // Test Deserialization for bad conf 460 | badEncoded := badConf.makeBad() 461 | 462 | if _, err := opaque.DeserializeConfiguration(badEncoded); err == nil || 463 | !strings.EqualFold(err.Error(), badConf.error) { 464 | t.Fatalf( 465 | "Expected error for %s. Want %q, got %q.\n\tEncoded: %v", 466 | badConf.name, 467 | badConf.error, 468 | err, 469 | badEncoded, 470 | ) 471 | } 472 | 473 | // Test bad configuration for client, server, and deserializer setup 474 | bad := convertToBadConf(badEncoded) 475 | 476 | if _, err := bad.Client(); err == nil || !strings.EqualFold(err.Error(), badConf.error) { 477 | t.Fatalf("Expected error for %s / client. Want %q, got %q", badConf.name, badConf.error, err) 478 | } 479 | 480 | if _, err := bad.Server(); err == nil || !strings.EqualFold(err.Error(), badConf.error) { 481 | t.Fatalf("Expected error for %s / server. Want %q, got %q", badConf.name, badConf.error, err) 482 | } 483 | 484 | if _, err := bad.Deserializer(); err == nil || !strings.EqualFold(err.Error(), badConf.error) { 485 | t.Fatalf("Expected error for %s / deserializer. Want %q, got %q", badConf.name, badConf.error, err) 486 | } 487 | }) 488 | } 489 | } 490 | 491 | func TestFakeRecord(t *testing.T) { 492 | // Test valid configurations 493 | testAll(t, func(t2 *testing.T, conf *configuration) { 494 | if _, err := conf.conf.GetFakeRecord(nil); err != nil { 495 | t.Fatalf("unexpected error on valid configuration: %v", err) 496 | } 497 | }) 498 | 499 | // Test for an invalid configuration. 500 | conf := &opaque.Configuration{ 501 | OPRF: 0, 502 | AKE: 0, 503 | KSF: 0, 504 | KDF: 0, 505 | MAC: 0, 506 | Hash: 0, 507 | Context: nil, 508 | } 509 | 510 | if _, err := conf.GetFakeRecord(nil); err == nil { 511 | t.Fatal("expected error on invalid configuration") 512 | } 513 | } 514 | -------------------------------------------------------------------------------- /tests/oprf_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright (C) 2020-2025 Daniel Bourdrez. All Rights Reserved. 4 | // 5 | // This source code is licensed under the MIT license found in the 6 | // LICENSE file in the root directory of this source tree or at 7 | // https://spdx.org/licenses/MIT.html 8 | 9 | package opaque_test 10 | 11 | import ( 12 | "bytes" 13 | "encoding/hex" 14 | "encoding/json" 15 | "fmt" 16 | "os" 17 | "strings" 18 | "testing" 19 | 20 | group "github.com/bytemare/ecc" 21 | 22 | "github.com/bytemare/opaque/internal/encoding" 23 | "github.com/bytemare/opaque/internal/oprf" 24 | "github.com/bytemare/opaque/internal/tag" 25 | ) 26 | 27 | type oprfVector struct { 28 | DST string `json:"groupDST"` 29 | Hash string `json:"hash"` 30 | KeyInfo string `json:"keyInfo"` 31 | Seed string `json:"seed"` 32 | SkSm string `json:"skSm"` 33 | SuiteName string `json:"suiteName"` 34 | SuiteID oprf.Identifier `json:"identifier"` 35 | Vectors []testVector `json:"vectors"` 36 | Mode byte `json:"mode"` 37 | } 38 | 39 | type test struct { 40 | Blind [][]byte 41 | BlindedElement [][]byte 42 | EvaluationElement [][]byte 43 | Input [][]byte 44 | Output [][]byte 45 | Batch int 46 | } 47 | 48 | type testVectors []oprfVector 49 | 50 | type testVector struct { 51 | Blind string `json:"Blind"` 52 | BlindedElement string `json:"BlindedElement"` 53 | EvaluationElement string `json:"EvaluationElement"` 54 | Input string `json:"Input"` 55 | Output string `json:"Output"` 56 | Batch int `json:"Batch"` 57 | } 58 | 59 | func decodeBatch(nb int, in string) ([][]byte, error) { 60 | v := strings.Split(in, ",") 61 | if len(v) != nb { 62 | return nil, fmt.Errorf("incoherent number of values in batch %d/%d", len(v), nb) 63 | } 64 | 65 | out := make([][]byte, nb) 66 | 67 | for i, s := range v { 68 | dec, err := hex.DecodeString(s) 69 | if err != nil { 70 | return nil, fmt.Errorf("hex decoding errored with %q", err) 71 | } 72 | out[i] = dec 73 | } 74 | 75 | return out, nil 76 | } 77 | 78 | func (tv *testVector) Decode() (*test, error) { 79 | blind, err := decodeBatch(tv.Batch, tv.Blind) 80 | // blind, err := hex.DecodeString(tv.Blind) 81 | if err != nil { 82 | return nil, fmt.Errorf(" Blind decoding errored with %q", err) 83 | } 84 | 85 | blinded, err := decodeBatch(tv.Batch, tv.BlindedElement) 86 | // blinded, err := hex.DecodeString(tv.BlindedElement) 87 | if err != nil { 88 | return nil, fmt.Errorf(" BlindedElement decoding errored with %q", err) 89 | } 90 | 91 | evaluationElement, err := decodeBatch(tv.Batch, tv.EvaluationElement) 92 | if err != nil { 93 | return nil, fmt.Errorf(" EvaluationElement decoding errored with %q", err) 94 | } 95 | 96 | input, err := decodeBatch(tv.Batch, tv.Input) 97 | // input, err := hex.DecodeString(tv.Input) 98 | if err != nil { 99 | return nil, fmt.Errorf(" Input decoding errored with %q", err) 100 | } 101 | 102 | output, err := decodeBatch(tv.Batch, tv.Output) 103 | // output, err := hex.DecodeString(tv.Output) 104 | if err != nil { 105 | return nil, fmt.Errorf(" Output decoding errored with %q", err) 106 | } 107 | 108 | return &test{ 109 | Batch: tv.Batch, 110 | Blind: blind, 111 | BlindedElement: blinded, 112 | EvaluationElement: evaluationElement, 113 | Input: input, 114 | Output: output, 115 | }, nil 116 | } 117 | 118 | func testBlind(t *testing.T, c oprf.Identifier, test *test) { 119 | client := c.Client() 120 | for i := 0; i < len(test.Input); i++ { 121 | s := c.Group().NewScalar() 122 | if err := s.Decode(test.Blind[i]); err != nil { 123 | t.Fatal(fmt.Errorf("blind decoding to scalar in suite %v errored with %q", c, err)) 124 | } 125 | 126 | blinded := client.Blind(test.Input[i], s).Encode() 127 | 128 | if !bytes.Equal(test.BlindedElement[i], blinded) { 129 | t.Fatal("unexpected blinded output") 130 | } 131 | } 132 | } 133 | 134 | func testEvaluation(t *testing.T, c oprf.Identifier, privKey *group.Scalar, test *test) { 135 | for i := 0; i < len(test.BlindedElement); i++ { 136 | b := c.Group().NewElement() 137 | if err := b.Decode(test.BlindedElement[i]); err != nil { 138 | t.Fatal(fmt.Errorf("blind decoding to element in suite %v errored with %q", c, err)) 139 | } 140 | 141 | ev := c.Evaluate(privKey, b) 142 | if !bytes.Equal(test.EvaluationElement[i], ev.Encode()) { 143 | t.Fatal("unexpected evaluation") 144 | } 145 | } 146 | } 147 | 148 | func testFinalization(t *testing.T, c oprf.Identifier, test *test) { 149 | client := c.Client() 150 | for i := 0; i < len(test.EvaluationElement); i++ { 151 | ev := c.Group().NewElement() 152 | if err := ev.Decode(test.EvaluationElement[i]); err != nil { 153 | t.Fatal(fmt.Errorf("blind decoding to element in suite %v errored with %q", c, err)) 154 | } 155 | 156 | s := c.Group().NewScalar() 157 | if err := s.Decode(test.Blind[i]); err != nil { 158 | t.Fatal(fmt.Errorf("blind decoding to scalar in suite %v errored with %q", c, err)) 159 | } 160 | 161 | client.Blind(test.Input[i], s) 162 | 163 | output := client.Finalize(ev) 164 | if !bytes.Equal(test.Output[i], output) { 165 | t.Fatal("unexpected output") 166 | } 167 | } 168 | } 169 | 170 | func getDST(prefix []byte, c oprf.Identifier) []byte { 171 | return encoding.Concatenate(prefix, []byte(tag.OPRFVersionPrefix), []byte(c)) 172 | } 173 | 174 | func (v oprfVector) test(t *testing.T) { 175 | s, err := hex.DecodeString(v.SkSm) 176 | if err != nil { 177 | t.Fatalf("private key decoding errored with %q\nfor sksm %v\n", err, v.SkSm) 178 | } 179 | 180 | privKey := v.SuiteID.Group().NewScalar() 181 | if err := privKey.Decode(s); err != nil { 182 | t.Fatal(fmt.Errorf("private key decoding to scalar in suite %v errored with %q", v.SuiteID, err)) 183 | } 184 | 185 | decSeed, err := hex.DecodeString(v.Seed) 186 | if err != nil { 187 | t.Fatalf("decoding errored with %q\nfor seed %v\n", err, v.Seed) 188 | } 189 | 190 | decKeyInfo, err := hex.DecodeString(v.KeyInfo) 191 | if err != nil { 192 | t.Fatalf("decoding errored with %q\nfor key info %v\n", err, v.KeyInfo) 193 | } 194 | 195 | sks := v.SuiteID.DeriveKey(decSeed, decKeyInfo) 196 | 197 | if !sks.Subtract(privKey).IsZero() { 198 | t.Fatalf(" DeriveKeyPair did not yield the expected key %v\n", hex.EncodeToString(sks.Encode())) 199 | } 200 | 201 | dst, err := hex.DecodeString(v.DST) 202 | if err != nil { 203 | t.Fatalf("hex decoding errored with %q", err) 204 | } 205 | 206 | dst2 := getDST([]byte(tag.OPRFPointPrefix), v.SuiteID) 207 | if !bytes.Equal(dst, dst2) { 208 | t.Fatalf( 209 | "GroupDST output is not valid.\n\twant: %v\n\tgot : %v", 210 | hex.EncodeToString(dst), 211 | hex.EncodeToString(dst2), 212 | ) 213 | } 214 | 215 | for i, tv := range v.Vectors { 216 | t.Run(fmt.Sprintf("Vector %d", i), func(t *testing.T) { 217 | test, err := tv.Decode() 218 | if err != nil { 219 | t.Fatal(fmt.Sprintf("batches : %v Failed %v\n", tv.Batch, err)) 220 | } 221 | 222 | // Test Blinding 223 | testBlind(t, v.SuiteID, test) 224 | 225 | // Server evaluating 226 | testEvaluation(t, v.SuiteID, privKey, test) 227 | 228 | // Client finalize 229 | testFinalization(t, v.SuiteID, test) 230 | }) 231 | } 232 | } 233 | 234 | func loadVOPRFVectors(filepath string) (testVectors, error) { 235 | contents, err := os.ReadFile(filepath) 236 | if err != nil { 237 | return nil, err 238 | } 239 | 240 | var v testVectors 241 | errJSON := json.Unmarshal(contents, &v) 242 | if errJSON != nil { 243 | return nil, errJSON 244 | } 245 | 246 | return v, nil 247 | } 248 | 249 | func TestVOPRFVectors(t *testing.T) { 250 | vectorFile := "oprfVectors.json" 251 | 252 | v, err := loadVOPRFVectors(vectorFile) 253 | if err != nil || v == nil { 254 | t.Fatal(err) 255 | } 256 | 257 | for _, tv := range v { 258 | if tv.Mode != 0x00 { 259 | continue 260 | } 261 | 262 | if tv.SuiteID == "decaf448-SHAKE256" { 263 | continue 264 | } 265 | 266 | t.Run(string(tv.Mode)+" - "+string(tv.SuiteID), tv.test) 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /tests/server_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright (C) 2020-2025 Daniel Bourdrez. All Rights Reserved. 4 | // 5 | // This source code is licensed under the MIT license found in the 6 | // LICENSE file in the root directory of this source tree or at 7 | // https://spdx.org/licenses/MIT.html 8 | 9 | package opaque_test 10 | 11 | import ( 12 | "errors" 13 | "strings" 14 | "testing" 15 | 16 | group "github.com/bytemare/ecc" 17 | 18 | "github.com/bytemare/opaque" 19 | "github.com/bytemare/opaque/internal" 20 | "github.com/bytemare/opaque/internal/encoding" 21 | ) 22 | 23 | var ( 24 | errInvalidStateLength = errors.New("invalid state length") 25 | errStateExists = errors.New("setting AKE state: existing state is not empty") 26 | ) 27 | 28 | /* 29 | The following tests look for failing conditions. 30 | */ 31 | 32 | func TestServer_BadRegistrationRequest(t *testing.T) { 33 | /* 34 | Error in OPRF 35 | - client blinded element invalid point encoding 36 | */ 37 | err1 := "invalid message length" 38 | err2 := "blinded data is an invalid point" 39 | 40 | testAll(t, func(t2 *testing.T, conf *configuration) { 41 | server, err := conf.conf.Server() 42 | if err != nil { 43 | t.Fatal(err) 44 | } 45 | if _, err := server.Deserialize.RegistrationRequest(nil); err == nil || !strings.HasPrefix(err.Error(), err1) { 46 | t.Fatalf("expected error. Got %v", err) 47 | } 48 | 49 | bad := getBadElement(t, conf) 50 | if _, err := server.Deserialize.RegistrationRequest(bad); err == nil || !strings.HasPrefix(err.Error(), err2) { 51 | t.Fatalf("expected error. Got %v", err) 52 | } 53 | }) 54 | } 55 | 56 | func TestServerInit_InvalidPublicKey(t *testing.T) { 57 | /* 58 | Nil and invalid server public key 59 | */ 60 | testAll(t, func(t2 *testing.T, conf *configuration) { 61 | server, err := conf.conf.Server() 62 | if err != nil { 63 | t.Fatal(err) 64 | } 65 | sk, _ := conf.conf.KeyGen() 66 | oprfSeed := internal.RandomBytes(conf.conf.Hash.Size()) 67 | 68 | expected := "input server public key's length is invalid" 69 | if err := server.SetKeyMaterial(nil, sk, nil, oprfSeed); err == nil || 70 | !strings.HasPrefix(err.Error(), expected) { 71 | t.Fatalf("expected error on nil pubkey - got %s", err) 72 | } 73 | 74 | expected = "invalid server public key: " 75 | if err := server.SetKeyMaterial(nil, sk, getBadElement(t, conf), oprfSeed); err == nil || 76 | !strings.HasPrefix(err.Error(), expected) { 77 | t.Fatalf("expected error on bad secret key - got %s", err) 78 | } 79 | }) 80 | } 81 | 82 | func TestServerInit_InvalidOPRFSeedLength(t *testing.T) { 83 | /* 84 | Nil and invalid server public key 85 | */ 86 | testAll(t, func(t2 *testing.T, conf *configuration) { 87 | server, err := conf.conf.Server() 88 | if err != nil { 89 | t.Fatal(err) 90 | } 91 | sk, pk := conf.conf.KeyGen() 92 | expected := opaque.ErrInvalidOPRFSeedLength 93 | 94 | if err := server.SetKeyMaterial(nil, sk, pk, nil); err == nil || !errors.Is(err, expected) { 95 | t.Fatalf("expected error on nil seed - got %s", err) 96 | } 97 | 98 | seed := internal.RandomBytes(conf.conf.Hash.Size() - 1) 99 | if err := server.SetKeyMaterial(nil, sk, pk, seed); err == nil || !errors.Is(err, expected) { 100 | t.Fatalf("expected error on bad seed - got %s", err) 101 | } 102 | 103 | seed = internal.RandomBytes(conf.conf.Hash.Size() + 1) 104 | if err := server.SetKeyMaterial(nil, sk, pk, seed); err == nil || !errors.Is(err, expected) { 105 | t.Fatalf("expected error on bad seed - got %s", err) 106 | } 107 | }) 108 | } 109 | 110 | func TestServerInit_NilSecretKey(t *testing.T) { 111 | /* 112 | Nil server secret key 113 | */ 114 | testAll(t, func(t2 *testing.T, conf *configuration) { 115 | server, err := conf.conf.Server() 116 | if err != nil { 117 | t.Fatal(err) 118 | } 119 | _, pk := conf.conf.KeyGen() 120 | expected := "invalid server AKE secret key: " 121 | 122 | if err := server.SetKeyMaterial(nil, nil, pk, nil); err == nil || 123 | !strings.HasPrefix(err.Error(), expected) { 124 | t.Fatalf("expected error on nil secret key - got %s", err) 125 | } 126 | }) 127 | } 128 | 129 | func TestServerInit_ZeroSecretKey(t *testing.T) { 130 | /* 131 | Nil server secret key 132 | */ 133 | testAll(t, func(t2 *testing.T, conf *configuration) { 134 | server, err := conf.conf.Server() 135 | if err != nil { 136 | t.Fatal(err) 137 | } 138 | sk := [32]byte{} 139 | 140 | var expected string 141 | 142 | switch conf.conf.AKE.Group() { 143 | case group.Ristretto255Sha512, group.P256Sha256: 144 | expected = "server private key is zero" 145 | default: 146 | expected = "invalid server AKE secret key: scalar Decode: invalid scalar length" 147 | } 148 | 149 | if err := server.SetKeyMaterial(nil, sk[:], nil, nil); err == nil || 150 | !strings.HasPrefix(err.Error(), expected) { 151 | t.Fatalf("expected error on nil secret key - got %s", err) 152 | } 153 | }) 154 | } 155 | 156 | func TestServerInit_NoKeyMaterial(t *testing.T) { 157 | /* 158 | SetKeyMaterial has not been called or was not successful 159 | */ 160 | testAll(t, func(t2 *testing.T, conf *configuration) { 161 | server, err := conf.conf.Server() 162 | if err != nil { 163 | t.Fatal(err) 164 | } 165 | expected := "key material not set: call SetKeyMaterial() to set values" 166 | 167 | if _, err := server.GenerateKE2(nil, nil); err == nil || 168 | !strings.HasPrefix(err.Error(), expected) { 169 | t.Fatalf("expected error not calling SetKeyMaterial - got %s", err) 170 | } 171 | }) 172 | } 173 | 174 | func TestServerInit_InvalidEnvelope(t *testing.T) { 175 | /* 176 | Record envelope of invalid length 177 | */ 178 | testAll(t, func(t2 *testing.T, conf *configuration) { 179 | server, err := conf.conf.Server() 180 | if err != nil { 181 | t.Fatal(err) 182 | } 183 | sk, pk := conf.conf.KeyGen() 184 | oprfSeed := internal.RandomBytes(conf.conf.Hash.Size()) 185 | 186 | if err := server.SetKeyMaterial(nil, sk, pk, oprfSeed); err != nil { 187 | t.Fatal(err) 188 | } 189 | 190 | client, err := conf.conf.Client() 191 | if err != nil { 192 | t.Fatal(err) 193 | } 194 | rec := buildRecord(internal.RandomBytes(32), oprfSeed, []byte("yo"), pk, client, server) 195 | rec.Envelope = internal.RandomBytes(15) 196 | 197 | expected := "record has invalid envelope length" 198 | if _, err := server.GenerateKE2(nil, rec); err == nil || 199 | !strings.HasPrefix(err.Error(), expected) { 200 | t.Fatalf("expected error on nil secret key - got %s", err) 201 | } 202 | }) 203 | } 204 | 205 | func TestServerInit_InvalidData(t *testing.T) { 206 | /* 207 | Invalid OPRF data in KE1 208 | */ 209 | testAll(t, func(t2 *testing.T, conf *configuration) { 210 | server, err := conf.conf.Server() 211 | if err != nil { 212 | t.Fatal(err) 213 | } 214 | ke1 := encoding.Concatenate( 215 | getBadElement(t, conf), 216 | internal.RandomBytes(server.GetConf().NonceLen), 217 | internal.RandomBytes(server.GetConf().Group.ElementLength()), 218 | ) 219 | expected := "blinded data is an invalid point" 220 | if _, err := server.Deserialize.KE1(ke1); err == nil || !strings.HasPrefix(err.Error(), expected) { 221 | t.Fatalf("expected error on bad oprf request - got %s", err) 222 | } 223 | }) 224 | } 225 | 226 | func TestServerInit_InvalidEPKU(t *testing.T) { 227 | /* 228 | Invalid EPKU in KE1 229 | */ 230 | testAll(t, func(t2 *testing.T, conf *configuration) { 231 | server, err := conf.conf.Server() 232 | if err != nil { 233 | t.Fatal(err) 234 | } 235 | client, err := conf.conf.Client() 236 | if err != nil { 237 | t.Fatal(err) 238 | } 239 | ke1 := client.GenerateKE1([]byte("yo")).Serialize() 240 | badke1 := encoding.Concat( 241 | ke1[:server.GetConf().OPRF.Group().ElementLength()+server.GetConf().NonceLen], 242 | getBadElement(t, conf), 243 | ) 244 | expected := "invalid ephemeral client public key" 245 | if _, err := server.Deserialize.KE1(badke1); err == nil || !strings.HasPrefix(err.Error(), expected) { 246 | t.Fatalf("expected error on bad epku - got %s", err) 247 | } 248 | }) 249 | } 250 | 251 | func TestServerFinish_InvalidKE3Mac(t *testing.T) { 252 | /* 253 | ke3 mac is invalid 254 | */ 255 | password := []byte("yo") 256 | conf := opaque.DefaultConfiguration() 257 | credId := internal.RandomBytes(32) 258 | oprfSeed := internal.RandomBytes(conf.Hash.Size()) 259 | client, _ := conf.Client() 260 | server, _ := conf.Server() 261 | sk, pk := conf.KeyGen() 262 | if err := server.SetKeyMaterial(nil, sk, pk, oprfSeed); err != nil { 263 | t.Fatal(err) 264 | } 265 | rec := buildRecord(credId, oprfSeed, password, pk, client, server) 266 | ke1 := client.GenerateKE1(password) 267 | ke2, err := server.GenerateKE2(ke1, rec) 268 | if err != nil { 269 | t.Fatal(err) 270 | } 271 | ke3, _, err := client.GenerateKE3(ke2) 272 | if err != nil { 273 | t.Fatal(err) 274 | } 275 | ke3.ClientMac[0] = ^ke3.ClientMac[0] 276 | 277 | expected := opaque.ErrAkeInvalidClientMac 278 | if err := server.LoginFinish(ke3); err == nil || err.Error() != expected.Error() { 279 | t.Fatalf("expected error on invalid mac - got %v", err) 280 | } 281 | } 282 | 283 | func TestServerSetAKEState_InvalidInput(t *testing.T) { 284 | conf := opaque.DefaultConfiguration() 285 | 286 | /* 287 | Test an invalid state 288 | */ 289 | 290 | buf := internal.RandomBytes(conf.MAC.Size() + conf.KDF.Size() + 1) 291 | 292 | server, _ := conf.Server() 293 | if err := server.SetAKEState(buf); err == nil || err.Error() != errInvalidStateLength.Error() { 294 | t.Fatalf("Expected error for SetAKEState. want %q, got %q", errInvalidStateLength, err) 295 | } 296 | 297 | /* 298 | A state already exists. 299 | */ 300 | password := []byte("yo") 301 | credId := internal.RandomBytes(32) 302 | seed := internal.RandomBytes(conf.Hash.Size()) 303 | client, _ := conf.Client() 304 | server, _ = conf.Server() 305 | sk, pk := conf.KeyGen() 306 | rec := buildRecord(credId, seed, password, pk, client, server) 307 | ke1 := client.GenerateKE1(password) 308 | _ = server.SetKeyMaterial(nil, sk, pk, seed) 309 | _, _ = server.GenerateKE2(ke1, rec) 310 | state := server.SerializeState() 311 | if err := server.SetAKEState(state); err == nil || err.Error() != errStateExists.Error() { 312 | t.Fatalf("Expected error for SetAKEState. want %q, got %q", errStateExists, err) 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /tests/testdata/fuzz/FuzzConfiguration/9c7be5871d57b46a: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("") 3 | []byte("0") 4 | uint(89) 5 | uint(5) 6 | uint(3) 7 | []byte("P256-SHA2\x016") 8 | byte('\x00') 9 | byte('\x03') 10 | -------------------------------------------------------------------------------- /tests/testdata/fuzz/FuzzConfiguration/f3f7abd74e23d56a: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("") 3 | []byte("0") 4 | uint(25) 5 | uint(82) 6 | uint(5) 7 | []byte("P256-SHA256") 8 | byte('-') 9 | byte('\x06') -------------------------------------------------------------------------------- /tests/testdata/fuzz/FuzzDeserializeKE1/fuzzbuzz-a1ae7e291f3d65e6ffdc8acb9f39f3538e3e0ab4: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("000000000x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000") 3 | []byte("0") 4 | uint(7) 5 | uint(7) 6 | uint(6) 7 | []byte("\x01") 8 | byte('\x00') 9 | byte('\x06') 10 | -------------------------------------------------------------------------------- /tests/testdata/fuzz/FuzzDeserializeKE2/fuzzbuzz-a1ae7e291f3d65e6ffdc8acb9f39f3538e3e0ab4: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("20000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") 3 | []byte("0") 4 | uint(7) 5 | uint(7) 6 | uint(7) 7 | []byte("\x01") 8 | byte('\x00') 9 | byte('\x06') 10 | -------------------------------------------------------------------------------- /tests/vectors_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // 3 | // Copyright (C) 2020-2025 Daniel Bourdrez. All Rights Reserved. 4 | // 5 | // This source code is licensed under the MIT license found in the 6 | // LICENSE file in the root directory of this source tree or at 7 | // https://spdx.org/licenses/MIT.html 8 | 9 | package opaque_test 10 | 11 | import ( 12 | "bytes" 13 | "crypto" 14 | "encoding/hex" 15 | "encoding/json" 16 | "fmt" 17 | "log" 18 | "os" 19 | "strings" 20 | "testing" 21 | 22 | "github.com/bytemare/hash" 23 | "github.com/bytemare/ksf" 24 | 25 | "github.com/bytemare/opaque" 26 | "github.com/bytemare/opaque/internal" 27 | "github.com/bytemare/opaque/internal/encoding" 28 | "github.com/bytemare/opaque/internal/oprf" 29 | ) 30 | 31 | type ByteToHex []byte 32 | 33 | func (j ByteToHex) MarshalJSON() ([]byte, error) { 34 | return json.Marshal(hex.EncodeToString(j)) 35 | } 36 | 37 | func (j *ByteToHex) UnmarshalJSON(b []byte) error { 38 | bs := strings.Trim(string(b), "\"") 39 | 40 | dst, err := hex.DecodeString(bs) 41 | if err != nil { 42 | return err 43 | } 44 | 45 | *j = dst 46 | return nil 47 | } 48 | 49 | /* 50 | Test the test vectors 51 | */ 52 | 53 | type config struct { 54 | Fake string `json:"Fake"` 55 | Group string `json:"Group"` 56 | Hash string `json:"Hash"` 57 | KDF string `json:"KDF"` 58 | MAC string `json:"MAC"` 59 | KSF string `json:"KSF"` 60 | Name string `json:"Name"` 61 | OPRF oprf.Identifier `json:"OPRF"` 62 | Context ByteToHex `json:"Context"` 63 | } 64 | 65 | type inputs struct { 66 | BlindLogin ByteToHex `json:"blind_login"` 67 | BlindRegistration ByteToHex `json:"blind_registration"` 68 | ClientIdentity ByteToHex `json:"client_identity,omitempty"` 69 | ClientKeyshareSeed ByteToHex `json:"client_keyshare_seed"` 70 | ClientNonce ByteToHex `json:"client_nonce"` 71 | CredentialIdentifier ByteToHex `json:"credential_identifier"` 72 | EnvelopeNonce ByteToHex `json:"envelope_nonce"` 73 | MaskingNonce ByteToHex `json:"masking_nonce"` 74 | OprfSeed ByteToHex `json:"oprf_seed"` 75 | Password ByteToHex `json:"password"` 76 | ServerIdentity ByteToHex `json:"server_identity,omitempty"` 77 | ServerKeyshareSeed ByteToHex `json:"server_keyshare_seed"` 78 | ServerNonce ByteToHex `json:"server_nonce"` 79 | ServerPrivateKey ByteToHex `json:"server_private_key"` 80 | ServerPublicKey ByteToHex `json:"server_public_key"` 81 | ClientPublicKey ByteToHex `json:"client_public_key"` // Used for fake credentials tests 82 | MaskingKey ByteToHex `json:"masking_key"` // Used for fake credentials tests 83 | KE1 ByteToHex `json:"KE1,omitempty"` // Used for fake credentials tests 84 | } 85 | 86 | type intermediates struct { 87 | AuthKey ByteToHex `json:"auth_key"` // 88 | ClientMacKey ByteToHex `json:"client_mac_key"` // 89 | ClientPublicKey ByteToHex `json:"client_public_key"` 90 | Envelope ByteToHex `json:"envelope"` // 91 | HandshakeSecret ByteToHex `json:"handshake_secret"` // 92 | MaskingKey ByteToHex `json:"masking_key"` 93 | OprfKey ByteToHex `json:"oprf_key"` 94 | RandomPWD ByteToHex `json:"randomized_pwd"` // 95 | ServerMacKey ByteToHex `json:"server_mac_key"` // 96 | } 97 | 98 | type outputs struct { 99 | KE1 ByteToHex `json:"KE1"` // 100 | KE2 ByteToHex `json:"KE2"` // 101 | KE3 ByteToHex `json:"KE3"` // 102 | ExportKey ByteToHex `json:"export_key"` // 103 | RegistrationRequest ByteToHex `json:"registration_request"` // 104 | RegistrationResponse ByteToHex `json:"registration_response"` // 105 | RegistrationRecord ByteToHex `json:"registration_upload"` // 106 | SessionKey ByteToHex `json:"session_key"` // 107 | } 108 | 109 | type vector struct { 110 | Config config `json:"config"` 111 | Inputs inputs `json:"inputs"` 112 | Intermediates intermediates `json:"intermediates"` 113 | Outputs outputs `json:"outputs"` 114 | } 115 | 116 | func (v *vector) testRegistration(conf *opaque.Configuration, t *testing.T) { 117 | // Client 118 | client, _ := conf.Client() 119 | 120 | g := conf.OPRF.Group() 121 | blind := g.NewScalar() 122 | if err := blind.Decode(v.Inputs.BlindRegistration); err != nil { 123 | panic(err) 124 | } 125 | 126 | regReq := client.RegistrationInit(v.Inputs.Password, opaque.ClientRegistrationInitOptions{OPRFBlind: blind}) 127 | 128 | if !bytes.Equal(v.Outputs.RegistrationRequest, regReq.Serialize()) { 129 | t.Fatalf( 130 | "registration requests do not match\nwant: %v\ngot : %v", 131 | hex.EncodeToString(v.Outputs.RegistrationRequest), 132 | hex.EncodeToString(regReq.Serialize()), 133 | ) 134 | } 135 | 136 | // Server 137 | server, _ := conf.Server() 138 | pks, err := server.Deserialize.DecodeAkePublicKey(v.Inputs.ServerPublicKey) 139 | if err != nil { 140 | panic(err) 141 | } 142 | 143 | regResp := server.RegistrationResponse(regReq, pks, v.Inputs.CredentialIdentifier, v.Inputs.OprfSeed) 144 | 145 | vRegResp, err := client.Deserialize.RegistrationResponse(v.Outputs.RegistrationResponse) 146 | if err != nil { 147 | t.Fatal(err) 148 | } 149 | 150 | if !bytes.Equal(vRegResp.EvaluatedMessage.Encode(), regResp.EvaluatedMessage.Encode()) { 151 | t.Logf("%v\n%v", vRegResp.EvaluatedMessage.Encode(), regResp.EvaluatedMessage.Encode()) 152 | t.Fatal("registration response data do not match") 153 | } 154 | 155 | if !bytes.Equal(vRegResp.Pks.Encode(), regResp.Pks.Encode()) { 156 | t.Fatal("registration response serverPublicKey do not match") 157 | } 158 | 159 | if !bytes.Equal(v.Outputs.RegistrationResponse, regResp.Serialize()) { 160 | t.Fatal("registration responses do not match") 161 | } 162 | 163 | // Client 164 | upload, exportKey := client.RegistrationFinalize( 165 | regResp, 166 | opaque.ClientRegistrationFinalizeOptions{ 167 | ClientIdentity: v.Inputs.ClientIdentity, 168 | ServerIdentity: v.Inputs.ServerIdentity, 169 | EnvelopeNonce: v.Inputs.EnvelopeNonce, 170 | }, 171 | ) 172 | 173 | if !bytes.Equal(v.Outputs.ExportKey, exportKey) { 174 | t.Fatalf("exportKey do not match\nexpected %v,\ngot %v", v.Outputs.ExportKey, exportKey) 175 | } 176 | 177 | if !bytes.Equal(v.Intermediates.Envelope, upload.Envelope) { 178 | t.Fatalf("envelopes do not match\nexpected %v,\ngot %v", v.Intermediates.Envelope, upload.Envelope) 179 | } 180 | 181 | if !bytes.Equal(v.Outputs.RegistrationRecord, upload.Serialize()) { 182 | t.Fatalf("registration upload do not match") 183 | } 184 | } 185 | 186 | func getFakeEnvelope(c *opaque.Configuration) []byte { 187 | if !hash.Hash(c.MAC).Available() { 188 | panic(nil) 189 | } 190 | 191 | envelopeSize := internal.NonceLength + internal.NewMac(c.MAC).Size() 192 | 193 | return make([]byte, envelopeSize) 194 | } 195 | 196 | func (v *vector) testLogin(conf *opaque.Configuration, t *testing.T) { 197 | // Client 198 | client, _ := conf.Client() 199 | 200 | if !isFake(v.Config.Fake) { 201 | g := conf.OPRF.Group() 202 | blind := g.NewScalar() 203 | if err := blind.Decode(v.Inputs.BlindLogin); err != nil { 204 | panic(err) 205 | } 206 | 207 | KE1 := client.GenerateKE1(v.Inputs.Password, opaque.GenerateKE1Options{ 208 | OPRFBlind: blind, 209 | KeyShareSeed: v.Inputs.ClientKeyshareSeed, 210 | AKENonce: v.Inputs.ClientNonce, 211 | AKENonceLength: internal.NonceLength, 212 | }) 213 | 214 | if !bytes.Equal(v.Outputs.KE1, KE1.Serialize()) { 215 | t.Fatalf("KE1 do not match") 216 | } 217 | } 218 | 219 | // Server 220 | server, _ := conf.Server() 221 | 222 | record := &opaque.ClientRecord{} 223 | if !isFake(v.Config.Fake) { 224 | upload, err := server.Deserialize.RegistrationRecord(v.Outputs.RegistrationRecord) 225 | if err != nil { 226 | t.Fatal(err) 227 | } 228 | 229 | record.RegistrationRecord = upload 230 | } else { 231 | rec, err := server.Deserialize.RegistrationRecord(encoding.Concat3(v.Inputs.ClientPublicKey, v.Inputs.MaskingKey, getFakeEnvelope(conf))) 232 | if err != nil { 233 | t.Fatal(err) 234 | } 235 | 236 | record.RegistrationRecord = rec 237 | } 238 | 239 | record.CredentialIdentifier = v.Inputs.CredentialIdentifier 240 | record.ClientIdentity = v.Inputs.ClientIdentity 241 | 242 | v.loginResponse(t, server, record) 243 | 244 | if isFake(v.Config.Fake) { 245 | return 246 | } 247 | 248 | // Client 249 | cke2, err := client.Deserialize.KE2(v.Outputs.KE2) 250 | if err != nil { 251 | t.Fatal(err) 252 | } 253 | 254 | ke3, exportKey, err := client.GenerateKE3( 255 | cke2, 256 | opaque.GenerateKE3Options{ 257 | ClientIdentity: v.Inputs.ClientIdentity, 258 | ServerIdentity: v.Inputs.ServerIdentity, 259 | }, 260 | ) 261 | if err != nil { 262 | t.Fatal(err) 263 | } 264 | 265 | //if !bytes.Equal(v.Intermediates.ClientMacKey, client.Ake.ClientMacKey) { 266 | // t.Fatal("client mac keys do not match") 267 | //} 268 | 269 | if !bytes.Equal(v.Outputs.ExportKey, exportKey) { 270 | t.Fatal("Client export keys do not match") 271 | } 272 | 273 | if !bytes.Equal(v.Outputs.SessionKey, client.SessionKey()) { 274 | t.Fatal("Client session keys do not match") 275 | } 276 | 277 | if !bytes.Equal(v.Outputs.KE3, ke3.Serialize()) { 278 | t.Fatal("KE3 do not match") 279 | } 280 | 281 | if err := server.LoginFinish(ke3); err != nil { 282 | t.Fatal(err) 283 | } 284 | 285 | if !bytes.Equal(v.Outputs.SessionKey, server.SessionKey()) { 286 | t.Fatal("Server session keys do not match") 287 | } 288 | } 289 | 290 | func oprfToGroup(oprf oprf.Identifier) opaque.Group { 291 | switch oprf { 292 | case "ristretto255-SHA512": 293 | return opaque.RistrettoSha512 294 | case "P256-SHA256": 295 | return opaque.P256Sha256 296 | default: 297 | return 0 298 | } 299 | } 300 | 301 | func (v *vector) test(t *testing.T) { 302 | p := &opaque.Configuration{ 303 | OPRF: oprfToGroup(v.Config.OPRF), 304 | AKE: groupToGroup(v.Config.Group), 305 | KSF: ksfToKSF(v.Config.KSF), 306 | Hash: hashToHash(v.Config.Hash), 307 | KDF: kdfToHash(v.Config.KDF), 308 | MAC: macToHash(v.Config.MAC), 309 | Context: []byte(v.Config.Context), 310 | } 311 | 312 | // Registration 313 | if !isFake(v.Config.Fake) { 314 | v.testRegistration(p, t) 315 | } 316 | 317 | if isFake(v.Config.Fake) { 318 | v.Outputs.KE1 = v.Inputs.KE1 319 | } 320 | 321 | // Login 322 | v.testLogin(p, t) 323 | } 324 | 325 | func (v *vector) loginResponse(t *testing.T, s *opaque.Server, record *opaque.ClientRecord) { 326 | ke1, err := s.Deserialize.KE1(v.Outputs.KE1) 327 | if err != nil { 328 | t.Fatal(err) 329 | } 330 | 331 | if err := s.SetKeyMaterial( 332 | v.Inputs.ServerIdentity, 333 | v.Inputs.ServerPrivateKey, 334 | v.Inputs.ServerPublicKey, 335 | v.Inputs.OprfSeed); err != nil { 336 | t.Fatal(err) 337 | } 338 | 339 | ke2, err := s.GenerateKE2( 340 | ke1, 341 | record, 342 | opaque.GenerateKE2Options{ 343 | KeyShareSeed: v.Inputs.ServerKeyshareSeed, 344 | AKENonce: v.Inputs.ServerNonce, 345 | AKENonceLength: internal.NonceLength, 346 | MaskingNonce: v.Inputs.MaskingNonce, 347 | }, 348 | ) 349 | if err != nil { 350 | t.Fatal(err) 351 | } 352 | 353 | //if !bytes.Equal(v.Intermediates.HandshakeSecret, s.Ake.HandshakeSecret) { 354 | // t.Fatalf("HandshakeSecrets do not match : %v", s.Ake.HandshakeSecret) 355 | //} 356 | 357 | //if !bytes.Equal(v.Intermediates.ServerMacKey, s.Ake.ServerMacKey) { 358 | // t.Fatalf("ServerMacs do not match.expected %v,\ngot %v", v.Intermediates.ServerMacKey, s.Ake.ServerMacKey) 359 | //} 360 | 361 | //if !bytes.Equal(v.Intermediates.ClientMacKey, s.Ake.Keys.ClientMacKey) { 362 | // t.Fatal("ClientMacs do not match") 363 | //} 364 | 365 | if !isFake(v.Config.Fake) { 366 | vectorKE3, err := s.Deserialize.KE3(v.Outputs.KE3) 367 | if err != nil { 368 | t.Fatal(err) 369 | } 370 | 371 | if !bytes.Equal(vectorKE3.ClientMac, s.ExpectedMAC()) { 372 | t.Fatalf("Expected client MACs do not match : %v", s.ExpectedMAC()) 373 | } 374 | 375 | if !bytes.Equal(v.Outputs.SessionKey, s.SessionKey()) { 376 | t.Fatalf("Server's session key is invalid : %v", v.Outputs.SessionKey) 377 | } 378 | } 379 | 380 | vectorKE2, err := s.Deserialize.KE2(v.Outputs.KE2) 381 | if err != nil { 382 | t.Fatal(err) 383 | } 384 | 385 | if !bytes.Equal( 386 | vectorKE2.CredentialResponse.EvaluatedMessage.Encode(), 387 | ke2.CredentialResponse.EvaluatedMessage.Encode(), 388 | ) { 389 | t.Fatal("data do not match") 390 | } 391 | 392 | if !bytes.Equal(vectorKE2.CredentialResponse.MaskingNonce, ke2.CredentialResponse.MaskingNonce) { 393 | t.Fatal("serverPublicKey do not match") 394 | } 395 | 396 | if !bytes.Equal(vectorKE2.CredentialResponse.MaskedResponse, ke2.CredentialResponse.MaskedResponse) { 397 | t.Fatal("MaskedResponse do not match") 398 | } 399 | 400 | if !bytes.Equal(vectorKE2.CredentialResponse.Serialize(), ke2.CredentialResponse.Serialize()) { 401 | t.Fatal("CredResp do not match") 402 | } 403 | 404 | if !bytes.Equal(vectorKE2.ServerNonce, ke2.ServerNonce) { 405 | t.Fatal("nonces do not match") 406 | } 407 | 408 | if !bytes.Equal(vectorKE2.ServerPublicKeyshare.Encode(), ke2.ServerPublicKeyshare.Encode()) { 409 | t.Fatal("epks do not match") 410 | } 411 | 412 | if !bytes.Equal(vectorKE2.ServerMac, ke2.ServerMac) { 413 | t.Fatalf("server macs do not match") 414 | } 415 | 416 | if !bytes.Equal(v.Outputs.KE2, ke2.Serialize()) { 417 | t.Fatalf("KE2 do not match") 418 | } 419 | 420 | if !isFake(v.Config.Fake) && !bytes.Equal(v.Outputs.SessionKey, s.Ake.SessionKey()) { 421 | t.Fatalf("Server SessionKey do not match:\n%v\n%v", v.Outputs.SessionKey, s.Ake.SessionKey()) 422 | } 423 | } 424 | 425 | func isFake(f string) bool { 426 | switch f { 427 | case "True": 428 | return true 429 | case "False": 430 | return false 431 | default: 432 | panic("'Fake' parameter not recognised") 433 | } 434 | } 435 | 436 | func hashToHash(h string) crypto.Hash { 437 | switch h { 438 | case "SHA256": 439 | return crypto.SHA256 440 | case "SHA512": 441 | return crypto.SHA512 442 | default: 443 | return 0 444 | } 445 | } 446 | 447 | func kdfToHash(h string) crypto.Hash { 448 | switch h { 449 | case "HKDF-SHA256": 450 | return crypto.SHA256 451 | case "HKDF-SHA512": 452 | return crypto.SHA512 453 | default: 454 | return 0 455 | } 456 | } 457 | 458 | func macToHash(h string) crypto.Hash { 459 | switch h { 460 | case "HMAC-SHA256": 461 | return crypto.SHA256 462 | case "HMAC-SHA512": 463 | return crypto.SHA512 464 | default: 465 | return 0 466 | } 467 | } 468 | 469 | func ksfToKSF(h string) ksf.Identifier { 470 | switch h { 471 | case "Identity": 472 | return 0 473 | case "Scrypt": 474 | return ksf.Scrypt 475 | default: 476 | return 0 477 | } 478 | } 479 | 480 | func groupToGroup(g string) opaque.Group { 481 | switch g { 482 | case "ristretto255": 483 | return opaque.RistrettoSha512 484 | case "decaf448": 485 | panic("group not supported") 486 | case "P256_XMD:SHA-256_SSWU_RO_": 487 | return opaque.P256Sha256 488 | case "P384_XMD:SHA-384_SSWU_RO_": 489 | return opaque.P384Sha512 490 | case "P521_XMD:SHA-512_SSWU_RO_": 491 | return opaque.P521Sha512 492 | // case "curve25519_XMD:SHA-512_ELL2_RO_": 493 | // return opaque.Curve25519Sha512 494 | default: 495 | log.Printf("group %s", g) 496 | panic("group not recognised") 497 | } 498 | } 499 | 500 | type draftVectors []*vector 501 | 502 | func loadOpaqueVectors(filepath string) (draftVectors, error) { 503 | contents, err := os.ReadFile(filepath) 504 | if err != nil { 505 | return nil, err 506 | } 507 | 508 | var v draftVectors 509 | errJSON := json.Unmarshal(contents, &v) 510 | if errJSON != nil { 511 | return nil, errJSON 512 | } 513 | 514 | return v, nil 515 | } 516 | 517 | func TestOpaqueVectors(t *testing.T) { 518 | vectorFile := "vectors.json" 519 | 520 | v, err := loadOpaqueVectors(vectorFile) 521 | if err != nil || v == nil { 522 | t.Fatal(err) 523 | } 524 | 525 | for _, tv := range v { 526 | if tv.Config.Group == "curve25519" { 527 | continue 528 | } 529 | t.Run(fmt.Sprintf("%s - %s - Fake:%s", tv.Config.Name, tv.Config.Group, tv.Config.Fake), tv.test) 530 | } 531 | } 532 | --------------------------------------------------------------------------------