Skip to content

Commit 47e8a64

Browse files
committed
fix(x509): handle hex.DecodeString error in FormatOID() instead of discarding
Malformed hex strings in certificate subject fields could previously produce corrupt decoded output because the error from hex.DecodeString was discarded with `decoded, _ := ...`. In a compliance ledger this is a security concern: an attacker-controlled or accidentally truncated certificate could yield bogus normalized OIDs in indexed fields. After this fix, malformed entries are skipped and the original (unmodified) subject value remains in the output, so consumers see the raw OID rather than a silently-corrupted normalized one. Adds regression tests to x/pki/x509/x509_test.go that exercise the bug: - Test_FormatOID_HexDecodeError: 5 cases covering non-hex chars, odd-length hex, embedded whitespace, mixed valid/invalid OIDs, and the positive-path regression guard. - Test_FormatOID_NoCorruptOutput: explicit assertion that the buggy output pattern ("vid=0x" with empty/null bytes) cannot occur. These tests fail with the pre-fix code and pass with the fix applied. Per reviewer feedback: removed unrelated x/audit module changes from this PR — they will be submitted separately if there's interest. Signed-off-by: Srikanth Patchava <srpatcha@users.noreply.github.com>
1 parent 94b3b50 commit 47e8a64

2 files changed

Lines changed: 87 additions & 2 deletions

File tree

x/pki/x509/x509.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,10 @@ func FormatOID(header, oldKey, newKey string) string {
153153
// get value from header
154154
value = value[len(value)-hexStringIntegerLength:]
155155

156-
decoded, _ := hex.DecodeString(value)
156+
decoded, err := hex.DecodeString(value)
157+
if err != nil {
158+
continue
159+
}
157160

158161
subjectValues[index] = fmt.Sprintf("%s=0x%s", newKey, decoded)
159162
}
@@ -234,7 +237,7 @@ func ParseAndValidateCertificate(pemCertificate string) (*Certificate, error) {
234237
serial := cert.SerialNumber
235238
// RFC 5280 requires serial numbers to be positive
236239
if serial.Sign() <= 0 {
237-
return pkitypes.NewErrInvalidCertificate("serial number must be a positive")
240+
return pkitypes.NewErrInvalidCertificate("serial number must be positive")
238241
}
239242

240243
// When crypto/x509 parses a certificate, it reads the DER integer, strips the sign byte if present,

x/pki/x509/x509_test.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,3 +336,85 @@ func Test_ParseAndValidateCertificate(t *testing.T) {
336336
}
337337
}
338338
}
339+
340+
// Test_FormatOID_HexDecodeError reproduces the bug fixed in this PR: previously
341+
// FormatOID() discarded the error from hex.DecodeString() with `decoded, _ := ...`,
342+
// causing malformed hex values in certificate subject fields to silently produce
343+
// corrupt decoded output (e.g. "vid=0x" with empty bytes). In a compliance ledger
344+
// this is a security concern because an attacker-controlled or accidentally
345+
// truncated certificate could yield bogus normalized OIDs.
346+
//
347+
// After the fix, malformed entries are skipped: the original value remains in
348+
// the output unchanged.
349+
func Test_FormatOID_HexDecodeError(t *testing.T) {
350+
tests := []struct {
351+
name string
352+
header string
353+
oldKey string
354+
newKey string
355+
// Expected output: original entry must be preserved unchanged when
356+
// hex.DecodeString cannot parse the trailing 8 chars.
357+
expected string
358+
}{
359+
{
360+
name: "non-hex characters in last 8 chars are skipped",
361+
header: "CN=Matter PAA 1,O=Google,C=US,1.3.6.1.4.1.37244.2.1=#1304ZZNOTHEX",
362+
oldKey: "1.3.6.1.4.1.37244.2.1",
363+
newKey: "vid",
364+
expected: "CN=Matter PAA 1,O=Google,C=US,1.3.6.1.4.1.37244.2.1=#1304ZZNOTHEX",
365+
},
366+
{
367+
name: "odd-length hex (invalid) is skipped",
368+
header: "CN=Matter PAA 1,1.3.6.1.4.1.37244.2.1=#13043600060",
369+
oldKey: "1.3.6.1.4.1.37244.2.1",
370+
newKey: "vid",
371+
expected: "CN=Matter PAA 1,1.3.6.1.4.1.37244.2.1=#13043600060",
372+
},
373+
{
374+
name: "value containing whitespace yields decode error",
375+
header: "CN=Matter PAA 1,1.3.6.1.4.1.37244.2.1=# 1304600",
376+
oldKey: "1.3.6.1.4.1.37244.2.1",
377+
newKey: "vid",
378+
expected: "CN=Matter PAA 1,1.3.6.1.4.1.37244.2.1=# 1304600",
379+
},
380+
{
381+
name: "valid hex still works (regression guard for positive path)",
382+
header: "CN=Matter PAA 1,O=Google,C=US,1.3.6.1.4.1.37244.2.1=#130436303036",
383+
oldKey: "1.3.6.1.4.1.37244.2.1",
384+
newKey: "vid",
385+
expected: "CN=Matter PAA 1,O=Google,C=US,vid=0x6006",
386+
},
387+
{
388+
name: "multiple OIDs - one valid, one invalid",
389+
header: "CN=Test,1.3.6.1.4.1.37244.2.1=#130436303036,1.3.6.1.4.1.37244.2.2=#1304ZZZZZZ",
390+
oldKey: "1.3.6.1.4.1.37244.2.2",
391+
newKey: "pid",
392+
expected: "CN=Test,1.3.6.1.4.1.37244.2.1=#130436303036,1.3.6.1.4.1.37244.2.2=#1304ZZZZZZ",
393+
},
394+
}
395+
396+
for _, tt := range tests {
397+
t.Run(tt.name, func(t *testing.T) {
398+
result := FormatOID(tt.header, tt.oldKey, tt.newKey)
399+
require.Equal(t, tt.expected, result,
400+
"FormatOID must NOT silently emit corrupted output when hex.DecodeString fails")
401+
})
402+
}
403+
}
404+
405+
// Test_FormatOID_NoCorruptOutput is an explicit assertion that the post-fix
406+
// behaviour cannot produce the buggy output pattern "newKey=0x" (empty hex)
407+
// which the pre-fix code could emit when hex.DecodeString returned an error.
408+
func Test_FormatOID_NoCorruptOutput(t *testing.T) {
409+
// Crafted input where the last 8 chars contain a non-hex sentinel.
410+
header := "CN=Test,1.3.6.1.4.1.37244.2.1=#1304NOTHEXX"
411+
result := FormatOID(header, "1.3.6.1.4.1.37244.2.1", "vid")
412+
413+
require.NotContains(t, result, "vid=0x ",
414+
"output must not contain 'vid=0x' followed by empty/whitespace bytes")
415+
require.NotContains(t, result, "vid=0x\x00",
416+
"output must not contain a null-byte after the hex prefix")
417+
// Original entry must survive untouched.
418+
require.Contains(t, result, "1.3.6.1.4.1.37244.2.1=#1304NOTHEXX",
419+
"malformed entry must be preserved verbatim, not silently rewritten")
420+
}

0 commit comments

Comments
 (0)