-
Notifications
You must be signed in to change notification settings - Fork 2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[tools] Implement Python version of spake2p tool (#23463)
Implement a Python script similar to spake2p tool for better portability and easier integration with build systems. The script allows one to generate SPAKE2+ verifier for a given passcode, salt and iteration count. Also, integrate the script with nRF Connect scripts for generating factory data and add a unit test. Signed-off-by: Damian Krolik <damian.krolik@nordicsemi.no> Signed-off-by: Damian Krolik <damian.krolik@nordicsemi.no>
- Loading branch information
1 parent
78ae9a4
commit a8b5eca
Showing
4 changed files
with
193 additions
and
19 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
# SPAKE2+ Python Tool | ||
|
||
SPAKE2+ Python Tool is a Python script for generating SPAKE2+ protocol | ||
parameters (only Verifier as of today). SPAKE2+ protocol is used during Matter | ||
commissioning to establish a secure session between the commissioner and the | ||
commissionee. | ||
|
||
## Usage Examples | ||
|
||
To list all available subcommands: | ||
|
||
```console | ||
$ ./spake2p.py --help | ||
usage: spake2p.py [-h] subcommand ... | ||
|
||
SPAKE2+ Python Tool | ||
|
||
positional arguments: | ||
subcommand | ||
gen-verifier Generate SPAKE2+ Verifier | ||
|
||
options: | ||
-h, --help show this help message and exit | ||
``` | ||
|
||
To display parameters of the `gen-verifier` subcommand: | ||
|
||
```console | ||
$ ./spake2p.py gen-verifier --help | ||
usage: spake2p.py gen-verifier [-h] -p PASSCODE -s SALT -i count | ||
|
||
options: | ||
-h, --help show this help message and exit | ||
-p PASSCODE, --passcode PASSCODE | ||
8-digit passcode | ||
-s SALT, --salt SALT Salt of length 16 to 32 octets encoded in Base64 | ||
-i count, --iteration-count count | ||
Iteration count between 1000 and 100000 | ||
``` | ||
|
||
To generate SPAKE2+ verifier for "SPAKE2P Key Salt" salt and 20202021 passcode, | ||
using 1000 PBKDF2 iterations: | ||
|
||
```console | ||
./spake2p.py gen-verifier -p 20202021 -s U1BBS0UyUCBLZXkgU2FsdA== -i 1000 | ||
uWFwqugDNGiEck/po7KHwwMwwqZgN10XuyBajPGuyzUEV/iree4lOrao5GuwnlQ65CJzbeUB49s31EH+NEkg0JVI5MGCQGMMT/SRPFNRODm3wH/MBiehuFc6FJ/NH6Rmzw== | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
#!/usr/bin/env python3 | ||
|
||
# | ||
# Copyright (c) 2022 Project CHIP Authors | ||
# All rights reserved. | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
# | ||
|
||
import argparse | ||
import base64 | ||
from ecdsa.curves import NIST256p | ||
import hashlib | ||
import struct | ||
|
||
# Forbidden passcodes as listed in the "5.1.7.1. Invalid Passcodes" section of the Matter spec | ||
INVALID_PASSCODES = [00000000, | ||
11111111, | ||
22222222, | ||
33333333, | ||
44444444, | ||
55555555, | ||
66666666, | ||
77777777, | ||
88888888, | ||
99999999, | ||
12345678, | ||
87654321, ] | ||
|
||
# Length of `w0s` and `w1s` elements | ||
WS_LENGTH = NIST256p.baselen + 8 | ||
|
||
|
||
def generate_verifier(passcode: int, salt: bytes, iterations: int) -> bytes: | ||
ws = hashlib.pbkdf2_hmac('sha256', struct.pack('<I', passcode), salt, iterations, WS_LENGTH * 2) | ||
w0 = int.from_bytes(ws[:WS_LENGTH], byteorder='big') % NIST256p.order | ||
w1 = int.from_bytes(ws[WS_LENGTH:], byteorder='big') % NIST256p.order | ||
L = NIST256p.generator * w1 | ||
|
||
return w0.to_bytes(NIST256p.baselen, byteorder='big') + L.to_bytes('uncompressed') | ||
|
||
|
||
def main(): | ||
def passcode_arg(arg: str) -> int: | ||
passcode = int(arg) | ||
|
||
if not 0 <= passcode <= 99999999: | ||
raise argparse.ArgumentTypeError('passcode out of range') | ||
|
||
if passcode in INVALID_PASSCODES: | ||
raise argparse.ArgumentTypeError('invalid passcode') | ||
|
||
return passcode | ||
|
||
def salt_arg(arg: str) -> bytes: | ||
salt = base64.b64decode(arg) | ||
|
||
if not 16 <= len(salt) <= 32: | ||
raise argparse.ArgumentTypeError('invalid salt length') | ||
|
||
return salt | ||
|
||
def iterations_arg(arg: str) -> int: | ||
iterations = int(arg) | ||
|
||
if not 1000 <= iterations <= 100000: | ||
raise argparse.ArgumentTypeError('iteration count out of range') | ||
|
||
return iterations | ||
|
||
parser = argparse.ArgumentParser(description='SPAKE2+ Python Tool', fromfile_prefix_chars='@') | ||
commands = parser.add_subparsers(dest='command', metavar='subcommand'.ljust(16), required=True) | ||
|
||
gen_verifier = commands.add_parser('gen-verifier', help='Generate SPAKE2+ Verifier') | ||
gen_verifier.add_argument('-p', '--passcode', type=passcode_arg, | ||
required=True, help='8-digit passcode') | ||
gen_verifier.add_argument('-s', '--salt', type=salt_arg, | ||
required=True, help='Salt of length 16 to 32 octets encoded in Base64') | ||
gen_verifier.add_argument('-i', '--iteration-count', type=iterations_arg, | ||
metavar='count', required=True, help='Iteration count between 1000 and 100000') | ||
|
||
args = parser.parse_args() | ||
|
||
if args.command == 'gen-verifier': | ||
verifier = generate_verifier(args.passcode, args.salt, args.iteration_count) | ||
print(base64.b64encode(verifier).decode('ascii')) | ||
|
||
|
||
if __name__ == '__main__': | ||
main() |