Skip to content

Commit d6dd362

Browse files
committed
fix: ensure blockchain hash determinism by refactoring UserName to string
Changed UserName field from *string to string to fix blockchain integrity validation issues. The pointer was causing non-deterministic hash computation due to memory address comparison instead of value comparison. This change: - Fixes blockchain validation for signatures with UserName - Simplifies code by removing pointer dereferencing logic - Maintains backward compatibility (NULL DB values map to empty string) - Updates i18n templates to display values directly
1 parent 6a292f7 commit d6dd362

File tree

9 files changed

+31
-51
lines changed

9 files changed

+31
-51
lines changed

internal/application/services/signature.go

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,6 @@ func (s *SignatureService) CreateSignature(ctx context.Context, request *models.
8585
logger.Logger.Info("Creating genesis signature (no previous signature)")
8686
}
8787

88-
var userName *string
89-
if request.User.Name != "" {
90-
userName = &request.User.Name
91-
}
92-
9388
logger.Logger.Info("Creating signature",
9489
"docID", request.DocID,
9590
"userSub", request.User.Sub,
@@ -100,7 +95,7 @@ func (s *SignatureService) CreateSignature(ctx context.Context, request *models.
10095
DocID: request.DocID,
10196
UserSub: request.User.Sub,
10297
UserEmail: request.User.NormalizedEmail(),
103-
UserName: userName,
98+
UserName: request.User.Name,
10499
SignedAtUTC: timestamp,
105100
PayloadHash: payloadHash,
106101
Signature: signatureB64,

internal/domain/models/signature.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ type Signature struct {
1515
DocID string `json:"doc_id" db:"doc_id"`
1616
UserSub string `json:"user_sub" db:"user_sub"`
1717
UserEmail string `json:"user_email" db:"user_email"`
18-
UserName *string `json:"user_name,omitempty" db:"user_name"`
18+
UserName string `json:"user_name,omitempty" db:"user_name"`
1919
SignedAtUTC time.Time `json:"signed_at" db:"signed_at"`
2020
PayloadHash string `json:"payload_hash" db:"payload_hash"`
2121
Signature string `json:"signature" db:"signature"`
@@ -47,7 +47,7 @@ type SignatureStatus struct {
4747

4848
// ComputeRecordHash Stable record hash supports tamper-evident chaining and integrity checks across migrations.
4949
func (s *Signature) ComputeRecordHash() string {
50-
data := fmt.Sprintf("%d|%s|%s|%s|%v|%s|%s|%s|%s|%s|%s",
50+
data := fmt.Sprintf("%d|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s",
5151
s.ID,
5252
s.DocID,
5353
s.UserSub,

internal/domain/models/signature_test.go

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import (
1111
func TestSignature_JSONSerialization(t *testing.T) {
1212
timestamp := time.Date(2024, 1, 15, 10, 30, 45, 123456789, time.UTC)
1313
createdAt := time.Date(2024, 1, 15, 10, 30, 46, 0, time.UTC)
14-
userName := "Test User"
1514
referer := "https://github.com/user/repo"
1615
prevHash := "abcd1234efgh5678"
1716

@@ -20,7 +19,7 @@ func TestSignature_JSONSerialization(t *testing.T) {
2019
DocID: "test-doc-123",
2120
UserSub: "google-oauth2|123456789",
2221
UserEmail: "test@example.com",
23-
UserName: &userName,
22+
UserName: "Test User",
2423
SignedAtUTC: timestamp,
2524
PayloadHash: "SGVsbG8gV29ybGQ=",
2625
Signature: "c2lnbmF0dXJlLWRhdGE=",
@@ -53,11 +52,8 @@ func TestSignature_JSONSerialization(t *testing.T) {
5352
if unmarshaled.UserEmail != signature.UserEmail {
5453
t.Errorf("UserEmail mismatch: got %v, expected %v", unmarshaled.UserEmail, signature.UserEmail)
5554
}
56-
if (unmarshaled.UserName == nil) != (signature.UserName == nil) {
57-
t.Errorf("UserName nil mismatch: got %v, expected %v", unmarshaled.UserName == nil, signature.UserName == nil)
58-
}
59-
if unmarshaled.UserName != nil && signature.UserName != nil && *unmarshaled.UserName != *signature.UserName {
60-
t.Errorf("UserName mismatch: got %v, expected %v", *unmarshaled.UserName, *signature.UserName)
55+
if unmarshaled.UserName != signature.UserName {
56+
t.Errorf("UserName mismatch: got %v, expected %v", unmarshaled.UserName, signature.UserName)
6157
}
6258
if !unmarshaled.SignedAtUTC.Equal(signature.SignedAtUTC) {
6359
t.Errorf("SignedAtUTC mismatch: got %v, expected %v", unmarshaled.SignedAtUTC, signature.SignedAtUTC)
@@ -97,7 +93,7 @@ func TestSignature_JSONSerializationWithNilFields(t *testing.T) {
9793
DocID: "minimal-doc",
9894
UserSub: "github|987654321",
9995
UserEmail: "minimal@example.com",
100-
UserName: nil,
96+
UserName: "",
10197
SignedAtUTC: timestamp,
10298
PayloadHash: "bWluaW1hbA==",
10399
Signature: "bWluaW1hbC1zaWc=",
@@ -129,8 +125,8 @@ func TestSignature_JSONSerializationWithNilFields(t *testing.T) {
129125
t.Fatalf("Failed to unmarshal signature: %v", err)
130126
}
131127

132-
if unmarshaled.UserName != nil {
133-
t.Errorf("UserName should be nil, got %v", unmarshaled.UserName)
128+
if unmarshaled.UserName != "" {
129+
t.Errorf("UserName should be empty string, got %v", unmarshaled.UserName)
134130
}
135131
if unmarshaled.Referer != nil {
136132
t.Errorf("Referer should be nil, got %v", unmarshaled.Referer)
@@ -242,15 +238,14 @@ func TestSignature_GetServiceInfo(t *testing.T) {
242238
func TestSignature_ComputeRecordHash(t *testing.T) {
243239
timestamp := time.Date(2024, 1, 15, 10, 30, 45, 123456789, time.UTC)
244240
createdAt := time.Date(2024, 1, 15, 10, 30, 46, 0, time.UTC)
245-
userName := "Test User"
246241
referer := "https://github.com/user/repo"
247242

248243
signature := &Signature{
249244
ID: 123,
250245
DocID: "test-doc-123",
251246
UserSub: "google-oauth2|123456789",
252247
UserEmail: "test@example.com",
253-
UserName: &userName,
248+
UserName: "Test User",
254249
SignedAtUTC: timestamp,
255250
PayloadHash: "SGVsbG8gV29ybGQ=",
256251
Signature: "c2lnbmF0dXJlLWRhdGE=",
@@ -282,13 +277,13 @@ func TestSignature_ComputeRecordHash(t *testing.T) {
282277
}
283278
signature.ID = originalID
284279

285-
signature.UserName = nil
286-
hashWithNilName := signature.ComputeRecordHash()
287-
if hashWithNilName == hash1 {
288-
t.Error("Hash should change when UserName becomes nil")
280+
signature.UserName = ""
281+
hashWithEmptyName := signature.ComputeRecordHash()
282+
if hashWithEmptyName == hash1 {
283+
t.Error("Hash should change when UserName becomes empty")
289284
}
290285

291-
signature.UserName = &userName
286+
signature.UserName = "Test User"
292287
signature.Referer = nil
293288
hashWithNilReferer := signature.ComputeRecordHash()
294289
if hashWithNilReferer == hash1 {
@@ -300,15 +295,14 @@ func TestSignature_ComputeRecordHashDeterministic(t *testing.T) {
300295
// Test that the same signature data produces the same hash
301296
timestamp := time.Date(2024, 1, 15, 10, 30, 45, 123456789, time.UTC)
302297
createdAt := time.Date(2024, 1, 15, 10, 30, 46, 0, time.UTC)
303-
userName := "Test User"
304298
referer := "https://github.com/user/repo"
305299

306300
sig1 := &Signature{
307301
ID: 123,
308302
DocID: "test-doc-123",
309303
UserSub: "google-oauth2|123456789",
310304
UserEmail: "test@example.com",
311-
UserName: &userName,
305+
UserName: "Test User",
312306
SignedAtUTC: timestamp,
313307
PayloadHash: "SGVsbG8gV29ybGQ=",
314308
Signature: "c2lnbmF0dXJlLWRhdGE=",
@@ -322,7 +316,7 @@ func TestSignature_ComputeRecordHashDeterministic(t *testing.T) {
322316
DocID: "test-doc-123",
323317
UserSub: "google-oauth2|123456789",
324318
UserEmail: "test@example.com",
325-
UserName: &userName,
319+
UserName: "Test User",
326320
SignedAtUTC: timestamp,
327321
PayloadHash: "SGVsbG8gV29ybGQ=",
328322
Signature: "c2lnbmF0dXJlLWRhdGE=",

internal/infrastructure/database/testutils.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -125,14 +125,13 @@ type SignatureFactory struct{}
125125

126126
func (f *SignatureFactory) CreateValidSignature() *models.Signature {
127127
now := time.Now().UTC()
128-
userName := "Test User"
129128
referer := "https://example.com/doc"
130129

131130
return &models.Signature{
132131
DocID: "test-doc-123",
133132
UserSub: "user-123",
134133
UserEmail: "test@example.com",
135-
UserName: &userName,
134+
UserName: "Test User",
136135
SignedAtUTC: now,
137136
PayloadHash: "dGVzdC1wYXlsb2FkLWhhc2g=", // base64("test-payload-hash")
138137
Signature: "dGVzdC1zaWduYXR1cmU=", // base64("test-signature")
@@ -176,7 +175,7 @@ func (f *SignatureFactory) CreateMinimalSignature() *models.Signature {
176175
DocID: "minimal-doc",
177176
UserSub: "minimal-user",
178177
UserEmail: "minimal@example.com",
179-
UserName: nil, // NULL
178+
UserName: "", // Empty string
180179
SignedAtUTC: now,
181180
PayloadHash: "bWluaW1hbA==", // base64("minimal")
182181
Signature: "bWluaW1hbA==", // base64("minimal")
@@ -202,8 +201,8 @@ func AssertSignatureEqual(t *testing.T, expected, actual *models.Signature) {
202201
t.Errorf("UserEmail mismatch: got %s, want %s", actual.UserEmail, expected.UserEmail)
203202
}
204203

205-
if !isStringPtrEqual(actual.UserName, expected.UserName) {
206-
t.Errorf("UserName mismatch: got %v, want %v", actual.UserName, expected.UserName)
204+
if actual.UserName != expected.UserName {
205+
t.Errorf("UserName mismatch: got %s, want %s", actual.UserName, expected.UserName)
207206
}
208207

209208
if actual.PayloadHash != expected.PayloadHash {

internal/presentation/handlers/oembed.go

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -95,12 +95,8 @@ func (h *OEmbedHandler) HandleOEmbed(w http.ResponseWriter, r *http.Request) {
9595
signatories := make([]SignatoryInfo, len(signatures))
9696
var lastSignedAt string
9797
for i, sig := range signatures {
98-
name := ""
99-
if sig.UserName != nil {
100-
name = *sig.UserName
101-
}
10298
signatories[i] = SignatoryInfo{
103-
Name: name,
99+
Name: sig.UserName,
104100
Email: sig.UserEmail,
105101
SignedAt: sig.SignedAtUTC.Format("02/01/2006 à 15:04"),
106102
}
@@ -173,12 +169,8 @@ func (h *OEmbedHandler) HandleEmbedView(w http.ResponseWriter, r *http.Request)
173169
signatories := make([]SignatoryInfo, len(signatures))
174170
var lastSignedAt string
175171
for i, sig := range signatures {
176-
name := ""
177-
if sig.UserName != nil {
178-
name = *sig.UserName
179-
}
180172
signatories[i] = SignatoryInfo{
181-
Name: name,
173+
Name: sig.UserName,
182174
Email: sig.UserEmail,
183175
SignedAt: sig.SignedAtUTC.Format("02/01/2006 à 15:04"),
184176
}

internal/presentation/handlers/signature.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -233,8 +233,8 @@ func (h *SignatureHandlers) HandleStatusJSON(w http.ResponseWriter, r *http.Requ
233233
"signed_at": sig.SignedAtUTC,
234234
}
235235

236-
if sig.UserName != nil && *sig.UserName != "" {
237-
sigData["user_name"] = *sig.UserName
236+
if sig.UserName != "" {
237+
sigData["user_name"] = sig.UserName
238238
}
239239

240240
if serviceInfo := sig.GetServiceInfo(); serviceInfo != nil {

locales/en.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,9 @@
6969
"admin_doc.no_signatures_title": "No signatures",
7070
"admin_doc.no_signatures_desc": "This document has not been signed yet.",
7171
"admin_doc.chain_integrity_valid": "Blockchain integrity:",
72-
"admin_doc.chain_integrity_count": "{{.ValidSigs}}/{{.TotalSigs}} valid signatures",
72+
"admin_doc.chain_integrity_count": "valid signatures",
7373
"admin_doc.chain_integrity_invalid": "Integrity issue detected:",
74-
"admin_doc.chain_integrity_errors": "{{.InvalidSigs}} invalid signature(s)",
74+
"admin_doc.chain_integrity_errors": "invalid signature(s)",
7575
"admin_doc.chain_errors_title": "Detected errors:",
7676

7777
"error.title": "Error",

locales/fr.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,9 @@
6969
"admin_doc.no_signatures_title": "Aucune signature",
7070
"admin_doc.no_signatures_desc": "Ce document n'a pas encore été signé.",
7171
"admin_doc.chain_integrity_valid": "Chaîne de blocs intègre :",
72-
"admin_doc.chain_integrity_count": "{{.ValidSigs}}/{{.TotalSigs}} signatures valides",
72+
"admin_doc.chain_integrity_count": "signatures valides",
7373
"admin_doc.chain_integrity_invalid": "Problème d'intégrité détecté :",
74-
"admin_doc.chain_integrity_errors": "{{.InvalidSigs}} signature(s) invalide(s)",
74+
"admin_doc.chain_integrity_errors": "signature(s) invalide(s)",
7575
"admin_doc.chain_errors_title": "Erreurs détectées :",
7676

7777
"error.title": "Erreur",

templates/admin_doc_details.html.tpl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@
104104
</div>
105105
<div class="ml-3">
106106
<p class="text-sm text-green-700">
107-
<strong>{{index .T "admin_doc.chain_integrity_valid"}}</strong> {{index .T "admin_doc.chain_integrity_count"}}
107+
<strong>{{index .T "admin_doc.chain_integrity_valid"}}</strong> {{.ChainIntegrity.ValidSigs}}/{{.ChainIntegrity.TotalSigs}} {{index .T "admin_doc.chain_integrity_count"}}
108108
</p>
109109
</div>
110110
</div>
@@ -119,7 +119,7 @@
119119
</div>
120120
<div class="ml-3">
121121
<p class="text-sm text-red-700">
122-
<strong>{{index .T "admin_doc.chain_integrity_invalid"}}</strong> {{index .T "admin_doc.chain_integrity_errors"}}
122+
<strong>{{index .T "admin_doc.chain_integrity_invalid"}}</strong> {{.ChainIntegrity.InvalidSigs}} {{index .T "admin_doc.chain_integrity_errors"}}
123123
</p>
124124
{{if .ChainIntegrity.Errors}}
125125
<div class="mt-2">

0 commit comments

Comments
 (0)