mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-02 08:16:57 +00:00
Two instances booting against a fresh database both find no key, both generate one, and both write. PutCryptoKey was last-writer-wins, so the loser kept its own key in memory while the database held the other's. It then signed OAuth client assertions with a key absent from the published JWKS, and issued registry JWTs that did not match the certificate written to disk. Every one of them fails verification, and nothing logs why. PutCryptoKey now keeps the first write, and both loaders re-read afterwards and use whatever is stored. Nothing in the codebase rotates a key through this function, so the update arm only ever fired on the race. Verified against the old behavior: with last-writer-wins restored, three of six concurrent loaders returned a key that was not the one in the database. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
39 lines
1.3 KiB
Go
39 lines
1.3 KiB
Go
package db
|
|
|
|
import "database/sql"
|
|
|
|
// GetCryptoKey retrieves a key by name from the database.
|
|
// Returns nil, nil if no key with that name exists.
|
|
func GetCryptoKey(db DBTX, name string) ([]byte, error) {
|
|
var data []byte
|
|
err := db.QueryRow("SELECT key_data FROM crypto_keys WHERE name = ?", name).Scan(&data)
|
|
if err == sql.ErrNoRows {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return data, nil
|
|
}
|
|
|
|
// PutCryptoKey stores a key in the database, keeping any key already stored
|
|
// under that name.
|
|
//
|
|
// First writer wins, deliberately. Two instances booting against a fresh
|
|
// database both find no key, both generate one, and both write. With
|
|
// last-writer-wins the loser kept its own key in memory while the database held
|
|
// the other's, so it signed JWTs with a key absent from the published JWKS and
|
|
// produced OAuth client assertions that fail verification. Nothing in the
|
|
// codebase rotates a key through this function, so the update arm only ever
|
|
// fired on that race.
|
|
//
|
|
// Callers must re-read after writing and use whatever is stored; see
|
|
// loadOAuthKey and loadRSAKey.
|
|
func PutCryptoKey(db DBTX, name string, data []byte) error {
|
|
_, err := db.Exec(
|
|
"INSERT INTO crypto_keys (name, key_data) VALUES (?, ?) ON CONFLICT(name) DO NOTHING",
|
|
name, data,
|
|
)
|
|
return err
|
|
}
|