This is an automated email from the ASF dual-hosted git repository.
gstein pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/steve.git
The following commit(s) were added to refs/heads/trunk by this push:
new a80c1be Switch/enforce 10-char hex strings for visible ID values.
a80c1be is described below
commit a80c1be29a4a9fd0fd9b850f509634c0ae5dc703
Author: Greg Stein <[email protected]>
AuthorDate: Sat Sep 20 06:18:52 2025 -0500
Switch/enforce 10-char hex strings for visible ID values.
- Election and Issue IDs are now 10-character hex strings. These will
often appear in a URL, so they could be cryptographically chosen.
We choose 40 bits so that expected-collision is over a million keys.
- add CHECK constraints, which take effect only on INSERT and UPDATE
- add crypto.create_id() for these standardized keys
- fix the coverage test for the new APIs and key requirements.
---
v3/schema.sql | 16 ++++++++++++----
v3/steve/crypto.py | 7 +++++++
v3/steve/election.py | 7 ++-----
v3/test/check_coverage.py | 31 ++++++++++++++++++-------------
4 files changed, 39 insertions(+), 22 deletions(-)
diff --git a/v3/schema.sql b/v3/schema.sql
index 52a57ff..d35e256 100644
--- a/v3/schema.sql
+++ b/v3/schema.sql
@@ -56,8 +56,8 @@
*/
CREATE TABLE elections (
- /* The Election ID. This is a unique text string. We do not use
- AUTOINCREMENT, so that URLs for elections cannot be deduced. */
+ /* The Election ID; 10 hex characters. We do not use AUTOINCREMENT,
+ so that URLs for Elections cannot be deduced. */
eid TEXT PRIMARY KEY NOT NULL,
/* Title of this election. */
@@ -88,6 +88,10 @@ CREATE TABLE elections (
opened). 1 for closed (implies it was opened). */
closed INTEGER,
+ /* Enforce the primary key as a 10-character (5 byte) hex string. */
+ CHECK (length(eid) = 10
+ AND eid GLOB
'[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]'),
+
/* Enforce/declare/document relationships. */
FOREIGN KEY (owner_pid) REFERENCES person(pid)
ON DELETE RESTRICT
@@ -100,8 +104,8 @@ CREATE TABLE elections (
/* The set of Issues to vote upon for a given Election. */
CREATE TABLE issues (
- /* The Issue ID, matching [-a-zA-Z0-9]+ */
- /* ### switch to autoincrement? use TITLE for humans. */
+ /* The Issue ID; 10 hex characters. We do not use AUTOINCREMENT,
+ so that URLs for Issues cannot be deduced. */
iid TEXT PRIMARY KEY NOT NULL,
/* Which election is this issue associated with? */
@@ -126,6 +130,10 @@ CREATE TABLE issues (
This will be NULL until the Election is opened. */
salt BLOB,
+ /* Enforce the primary key as a 10-character (5 byte) hex string. */
+ CHECK (length(iid) = 10
+ AND iid GLOB
'[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]'),
+
/* Enforce/declare/document relationships. */
FOREIGN KEY (eid) REFERENCES elections(eid)
ON DELETE RESTRICT
diff --git a/v3/steve/crypto.py b/v3/steve/crypto.py
index ce72123..4ea0baa 100644
--- a/v3/steve/crypto.py
+++ b/v3/steve/crypto.py
@@ -92,3 +92,10 @@ def shuffle(x):
# We shuffled in-place, but also return for funsies.
return x
+
+
+def create_id():
+ "Create a standard ID value."
+
+ # Use 10 hex characters for the ID
+ return secrets.token_hex(5) # 5 bytes
diff --git a/v3/steve/election.py b/v3/steve/election.py
index b8f0eda..d76d883 100644
--- a/v3/steve/election.py
+++ b/v3/steve/election.py
@@ -384,8 +384,5 @@ class Election:
return j and json.loads(j)
-def new_eid():
- "Create a new ElectionID."
-
- # Use 8 hex characters for an ElectionID.
- return secrets.token_hex(4) # 4 bytes
+### compat:
+new_eid = crypto.create_id
diff --git a/v3/test/check_coverage.py b/v3/test/check_coverage.py
index 75208da..3d50b86 100755
--- a/v3/test/check_coverage.py
+++ b/v3/test/check_coverage.py
@@ -42,8 +42,9 @@ SCHEMA_FILE = os.path.join(PARENT_DIR, 'schema.sql')
def touch_every_line():
"A minimal test to run each line in the 'steve' package."
- # Do the import *WITHIN* the coverage test.
+ # Do the imports *WITHIN* the coverage test.
import steve.election
+ import steve.crypto
eid = steve.election.new_eid()
@@ -71,8 +72,12 @@ def touch_every_line():
e.delete_person('david')
_ = e.get_person('alice')
- e.add_issue('a', eid, 'issue A', None, 'yna', None)
- e.add_issue('b', eid, 'issue B', None, 'stv', {
+ i1 = steve.crypto.create_id()
+ i2 = steve.crypto.create_id()
+ i3 = steve.crypto.create_id()
+
+ e.add_issue(i1, eid, 'issue A', None, 'yna', None)
+ e.add_issue(i2, eid, 'issue B', None, 'stv', {
'seats': 3,
'labelmap': {
'a': 'Alice',
@@ -83,24 +88,24 @@ def touch_every_line():
},
})
_ = e.list_issues()
- e.add_issue('c', eid, 'issue C', None, 'yna', None)
- e.delete_issue('c')
- _ = e.get_issue('a')
+ e.add_issue(i3, eid, 'issue C', None, 'yna', None)
+ e.delete_issue(i3)
+ _ = e.get_issue(i1)
e.open()
_ = e.get_metadata() # while OPEN
- e.add_vote('alice', 'a', 'y')
- e.add_vote('bob', 'a', 'n')
- e.add_vote('carlos', 'a', 'a') # use each of Y/N/A
- e.add_vote('alice', 'b', 'bc')
- e.add_vote('bob', 'b', 'ad')
+ e.add_vote('alice', i1, 'y')
+ e.add_vote('bob', i1, 'n')
+ e.add_vote('carlos', i1, 'a') # use each of Y/N/A
+ e.add_vote('alice', i2, 'bc')
+ e.add_vote('bob', i2, 'ad')
_ = e.has_voted_upon('alice')
_ = e.is_tampered()
e.close()
_ = e.get_metadata() # while CLOSED
- _ = e.tally_issue('a')
- _ = e.tally_issue('b')
+ _ = e.tally_issue(i1)
+ _ = e.tally_issue(i2)
def main():