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():

Reply via email to