├── .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 [](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 | [](https://github.com/bytemare/opaque/actions/workflows/wf-analysis.yaml)
3 | [](https://pkg.go.dev/github.com/bytemare/opaque)
4 | [](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 [](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 |
--------------------------------------------------------------------------------