mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-20 03:11:46 +00:00
feat: support different checksum format to support RHEL
This commit is contained in:
@@ -182,13 +182,21 @@ func fetchExpectedDigest(checksumURL, filename string) (string, error) {
|
||||
return digest, nil
|
||||
}
|
||||
|
||||
// parseSumsFile scans a coreutils-style sums file and returns the hex digest
|
||||
// matching filename. Filename match is case-sensitive: upstream Debian/Ubuntu/
|
||||
// Alpine sums are consistently lowercase and a divergence is a real signal.
|
||||
// parseSumsFile scans a sums file and returns the hex digest matching filename.
|
||||
// Filename match is case-sensitive: upstream Debian/Ubuntu/Alpine sums are
|
||||
// consistently lowercase and a divergence is a real signal.
|
||||
//
|
||||
// Accepts three on-the-wire shapes: "<hex> <name>" (text mode), "<hex> *<name>"
|
||||
// (binary mode), and a bare single-token "<hex>" line (single-file .sha512 from
|
||||
// Alpine, which does not carry a filename).
|
||||
// Accepts four on-the-wire shapes: "<hex> <name>" (coreutils text mode),
|
||||
// "<hex> *<name>" (coreutils binary mode), "<algo> (<name>) = <hex>" (BSD
|
||||
// style, used by Rocky/RHEL/Alma/Fedora/CentOS Stream CHECKSUM files), and a
|
||||
// bare single-token "<hex>" line (single-file .sha512 from Alpine, which does
|
||||
// not carry a filename).
|
||||
//
|
||||
// GPG cleartext-signed armor is tolerated implicitly: the BEGIN/END markers
|
||||
// and "Hash:" header don't match any shape's structural guards, and signature
|
||||
// body lines are individually single tokens but always appear in pairs
|
||||
// (base64 + the =CRC trailer at minimum), so bareCount stays ≥ 2 and the
|
||||
// bare-digest fallback never fires on a signed file.
|
||||
func parseSumsFile(body []byte, filename string) (string, error) {
|
||||
var bareDigest string
|
||||
bareCount := 0
|
||||
@@ -209,10 +217,21 @@ func parseSumsFile(body []byte, filename string) (string, error) {
|
||||
if name == filename {
|
||||
return fields[0], nil
|
||||
}
|
||||
case 4:
|
||||
// BSD-style: "<algo> (<name>) = <hex>". The "=" guard also rejects
|
||||
// armor lines like "-----BEGIN PGP SIGNED MESSAGE-----" that happen
|
||||
// to tokenise into 4 fields. Filenames have no spaces in upstream
|
||||
// usage (Rocky/Alma/Fedora pin dated builds).
|
||||
if fields[2] != "=" || !strings.HasPrefix(fields[1], "(") || !strings.HasSuffix(fields[1], ")") {
|
||||
continue
|
||||
}
|
||||
name := strings.TrimSuffix(strings.TrimPrefix(fields[1], "("), ")")
|
||||
if name == filename {
|
||||
return fields[3], nil
|
||||
}
|
||||
default:
|
||||
// More than two fields: not a format we recognise. Skip rather than
|
||||
// reject outright — signed sums files occasionally have trailing
|
||||
// commentary we want to tolerate.
|
||||
// Unrecognised shape. Skip rather than reject outright — signed sums
|
||||
// files occasionally have trailing commentary we want to tolerate.
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
|
||||
@@ -1464,4 +1464,122 @@ func TestVerifyImageChecksum(t *testing.T) {
|
||||
require.Error(t, err)
|
||||
assert.ErrorIs(t, err, ErrChecksumFetchFailed)
|
||||
})
|
||||
|
||||
t.Run("rocky bsd-style gpg-armored sums match", func(t *testing.T) {
|
||||
const rockyName = "Rocky-10.0-20250612.0.x86_64.qcow2"
|
||||
rockyBytes := []byte("rocky-image-bytes-fixture")
|
||||
img := writeTempImage(t, rockyName, rockyBytes)
|
||||
// Captured shape of a real Rocky CHECKSUM file: GPG cleartext-signed
|
||||
// armor wrapping BSD-style "SHA256 (name) = hex" lines, followed by an
|
||||
// ASCII-armored signature block we must skip without misparsing.
|
||||
body := fmt.Sprintf(`-----BEGIN PGP SIGNED MESSAGE-----
|
||||
Hash: SHA256
|
||||
|
||||
SHA256 (Rocky-10.0-20250612.0-x86_64-boot.iso) = %s
|
||||
SHA256 (%s) = %s
|
||||
SHA256 (Rocky-10.0-20250612.0-x86_64-dvd.iso) = %s
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iQIzBAEBCAAdFiEEnK6Ehq8eQ0z6yqv7tQAaIQAaIQAAaIQFAmJabcdEFGhijklm
|
||||
nopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ+/abcdefghijklmn
|
||||
=ABCD
|
||||
-----END PGP SIGNATURE-----
|
||||
`, sha256Hex([]byte("boot-iso")), rockyName, sha256Hex(rockyBytes), sha256Hex([]byte("dvd-iso")))
|
||||
url := newSumsServer(t, body)
|
||||
err := VerifyImageChecksum(img, url, "sha256")
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("alma bsd-style gpg-armored sums match", func(t *testing.T) {
|
||||
const almaName = "AlmaLinux-10-GenericCloud-10.0-x86_64.qcow2"
|
||||
almaBytes := []byte("alma-image-bytes-fixture")
|
||||
img := writeTempImage(t, almaName, almaBytes)
|
||||
body := fmt.Sprintf(`-----BEGIN PGP SIGNED MESSAGE-----
|
||||
Hash: SHA256
|
||||
|
||||
SHA256 (%s) = %s
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iHUEABYKAB0WIQTm5n8/yqv7tQAaIQAaIQAFAmJabcdEFGhijklmnopqrstuvwxyz
|
||||
=EFGH
|
||||
-----END PGP SIGNATURE-----
|
||||
`, almaName, sha256Hex(almaBytes))
|
||||
url := newSumsServer(t, body)
|
||||
err := VerifyImageChecksum(img, url, "sha256")
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("bsd-style filename absent", func(t *testing.T) {
|
||||
const rockyName = "Rocky-10.0-20250612.0.x86_64.qcow2"
|
||||
rockyBytes := []byte("rocky-image-bytes-fixture")
|
||||
img := writeTempImage(t, rockyName, rockyBytes)
|
||||
body := fmt.Sprintf(`-----BEGIN PGP SIGNED MESSAGE-----
|
||||
Hash: SHA256
|
||||
|
||||
SHA256 (Rocky-10.0-20250612.0-x86_64-boot.iso) = %s
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iQIzBAEBCAAdFiEEnK6Ehq8eQ0z6yqv7tQAaIQAaIQAAaIQFAmJabcdEFGhijklm
|
||||
=ABCD
|
||||
-----END PGP SIGNATURE-----
|
||||
`, sha256Hex([]byte("boot-iso")))
|
||||
url := newSumsServer(t, body)
|
||||
err := VerifyImageChecksum(img, url, "sha256")
|
||||
assert.ErrorIs(t, err, ErrChecksumNotFound)
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseSumsFile(t *testing.T) {
|
||||
const target = "Rocky-10.0-20250612.0.x86_64.qcow2"
|
||||
const targetHex = "a3f1c2d4e5f60718293a4b5c6d7e8f9012345678901234567890abcdefabcdef"
|
||||
|
||||
t.Run("bsd 4-field line", func(t *testing.T) {
|
||||
body := []byte("SHA256 (" + target + ") = " + targetHex + "\n")
|
||||
got, err := parseSumsFile(body, target)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, targetHex, got)
|
||||
})
|
||||
|
||||
t.Run("bsd missing equals separator skipped", func(t *testing.T) {
|
||||
// Malformed line — third field is not "=". Must not match.
|
||||
body := []byte("SHA256 (" + target + ") - " + targetHex + "\n")
|
||||
_, err := parseSumsFile(body, target)
|
||||
assert.ErrorIs(t, err, ErrChecksumNotFound)
|
||||
})
|
||||
|
||||
t.Run("bsd missing parens skipped", func(t *testing.T) {
|
||||
// Without parens the filename field is ambiguous; reject by not matching.
|
||||
body := []byte("SHA256 " + target + " = " + targetHex + "\n")
|
||||
_, err := parseSumsFile(body, target)
|
||||
assert.ErrorIs(t, err, ErrChecksumNotFound)
|
||||
})
|
||||
|
||||
t.Run("gpg-armored body with no sums returns not found", func(t *testing.T) {
|
||||
// Signature-block lines are individually single tokens. The
|
||||
// bare-digest fallback only fires when bareCount == 1; PGP signatures
|
||||
// always have ≥ 2 (the base64 block + the =CRC trailer), so the
|
||||
// fallback stays dormant and we correctly report NotFound.
|
||||
body := []byte(`-----BEGIN PGP SIGNED MESSAGE-----
|
||||
Hash: SHA256
|
||||
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iQIzBAEBCAAdFiEEnK6Ehq8eQ0z6yqv7tQAaIQAaIQAAaIQFAmJabcdEFGhijklm
|
||||
=ABCD
|
||||
-----END PGP SIGNATURE-----
|
||||
`)
|
||||
_, err := parseSumsFile(body, target)
|
||||
assert.ErrorIs(t, err, ErrChecksumNotFound)
|
||||
})
|
||||
|
||||
t.Run("mixed bsd and coreutils shapes", func(t *testing.T) {
|
||||
// Defensive: a sums file with both shapes should still resolve the
|
||||
// requested filename regardless of which line carries it.
|
||||
body := []byte("# header\n" +
|
||||
targetHex + " some-other.iso\n" +
|
||||
"SHA256 (" + target + ") = " + targetHex + "\n")
|
||||
got, err := parseSumsFile(body, target)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, targetHex, got)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user