Kryptografia klucza publicznego (asymetryczna) używa pary kluczy zamiast sekretu współdzielonego: cokolwiek podpisze klucz prywatny, zweryfikuje pasujący klucz publiczny; cokolwiek zostanie zapieczętowane pod klucz publiczny, otworzy wyłącznie pasujący klucz prywatny. Klucz publiczny można przekazać każdemu, także przeciwnikowi, bez osłabiania czegokolwiek.
RSA
Generowanie kluczy poszukuje dużych liczb pierwszych i jest naprawdę powolne, kilka sekund dla klucza 2048-bitowego; nigdy nie uruchamiaj go w wątku interfejsu użytkownika. Do szyfrowania stosuj OAEP, a do podpisów PSS, oba zawierają element losowy, którego brakuje w PKCS#1 v1.5. Podpisywanie/weryfikacja PKCS#1 v1.5 jest dostępne w sgcCrypto_RSA dla interoperacyjności z formatami, które go wymagają (typowym przypadkiem jest JWS RS256); do wszystkiego nowego preferuj PSS.
function sgcRSA_GenerateKey(aBits: Integer): TsgcRSAPrivateKey;
function sgcRSA_PublicKey(const aPrivateKey: TsgcRSAPrivateKey): TsgcRSAPublicKey;
function sgcRSA_OAEP_Encrypt(const aKey: TsgcRSAPublicKey; const aPlain, aLabel: TBytes; aHash: TsgcRSAHash): TBytes;
function sgcRSA_OAEP_Decrypt(const aKey: TsgcRSAPrivateKey; const aCipher, aLabel: TBytes; aHash: TsgcRSAHash; out aPlain: TBytes): Boolean;
function sgcRSA_PSS_Sign(const aKey: TsgcRSAPrivateKey; const aData: TBytes; aHash: TsgcRSAHash; aSaltLen: Integer = -1): TBytes;
function sgcRSA_PSS_Verify(const aKey: TsgcRSAPublicKey; const aData, aSignature: TBytes; aHash: TsgcRSAHash; aSaltLen: Integer = -1): Boolean;
var
oKey: TsgcRSAPrivateKey;
oSignature: TBytes;
begin
oKey := sgcRSA_GenerateKey(2048);
oSignature := sgcRSA_PSS_Sign(oKey, oData, rhSHA256);
if sgcRSA_PSS_Verify(sgcRSA_PublicKey(oKey), oData, oSignature, rhSHA256) then
{ valid };
end;
Dostępne jest również szyfrowanie PKCS#1 v1.5, tryb, który Java nazywa RSA/NONE/PKCS1Padding. Jest tu wyłącznie dla interoperacyjności z formatami, które go wymagają, i do niczego więcej: dopełnienie v1.5 jest dokładnie tym, w co celuje atak Bleichenbachera, więc do wszystkiego nowego właściwym wyborem pozostaje OAEP. Tekst jawny jest ograniczony do rozmiaru modułu pomniejszonego o 11 bajtów, czyli 245 bajtów przy kluczu 2048-bitowym.
function sgcRSA_PKCS1_Encrypt(const aKey: TsgcRSAPublicKey; const aPlain: TBytes): TBytes;
function sgcRSA_PKCS1_Decrypt(const aKey: TsgcRSAPrivateKey; const aCipher: TBytes; out aPlain: TBytes): Boolean;
sgcRSA_PKCS1_Decrypt zwraca False dla każdego rodzaju niepowodzenia dekodowania i przechodzi tę samą ścieżkę dla każdego z nich, ponieważ ich rozróżnienie jest dokładnie tym, czego potrzebuje padding oracle.
OAEP z osobnym skrótem MGF1
OAEP używa skrótu w dwóch miejscach, nad etykietą oraz wewnątrz funkcji generowania maski MGF1, i te dwa nie muszą być takie same. Właśnie w tym miejscu Java gryzie. SunJCE odczytuje OAEPWithSHA256AndMGF1Padding jako SHA-256 dla skrótu, przy czym MGF1 pozostaje na SHA-1, podczas gdy Bouncy Castle odczytuje ten sam ciąg jako SHA-256 dla obu, więc szyfrogram wytworzony przez jedną z tych implementacji jest nie do odszyfrowania przez drugą. Pięcioparametrowe przeciążenia ustalają oba skróty niezależnie, co bywa konieczne przy rozmowie z partnerem działającym w Javie. Czteroparametrowa forma pozostaje bez zmian i przekazuje aHash dla obu.
function sgcRSA_OAEP_Encrypt(const aKey: TsgcRSAPublicKey; const aPlain, aLabel: TBytes; aHash, aMGFHash: TsgcRSAHash): TBytes; overload;
function sgcRSA_OAEP_Decrypt(const aKey: TsgcRSAPrivateKey; const aCipher, aLabel: TBytes; aHash, aMGFHash: TsgcRSAHash; out aPlain: TBytes): Boolean; overload;
var
oKey: TsgcRSAPrivateKey;
oCipher: TBytes;
begin
{ SunJCE OAEPWithSHA256AndMGF1Padding: skrót etykiety SHA-256, MGF1 na SHA-1. }
oCipher := sgcRSA_OAEP_Encrypt(sgcRSA_PublicKey(oKey), oPlain, nil, rhSHA256, rhSHA1);
end;
Niezgodność skrótów jest nierozróżnialna od uszkodzonego szyfrogramu, więc błędny aMGFHash wraca jako zwykłe False, bez żadnej wskazówki, który z dwóch jest niepoprawny. Obie strony muszą uzgodnić go wcześniej.
Klucze eksportują się do PKCS#1 i SubjectPublicKeyInfo DER oraz do PEM:
function sgcRSA_ExportPrivateKeyPEM(const aKey: TsgcRSAPrivateKey): string;
function sgcRSA_ExportPublicKeyPEM(const aKey: TsgcRSAPublicKey): string;
PKCS#8 to drugi kontener klucza prywatnego, ten zapisywany jako BEGIN PRIVATE KEY, i to jego oczekują Java i .NET; para powyżej zapisuje zamiast tego starszy PKCS#1 BEGIN RSA PRIVATE KEY. Żaden z kontenerów nie jest zaszyfrowany, więc to, co wraca, to nagi materiał klucza, który nie powinien trafić tam, gdzie sięgnie log albo kopia zapasowa.
function sgcRSA_ExportPrivateKeyPKCS8DER(const aKey: TsgcRSAPrivateKey): TBytes;
function sgcRSA_ExportPrivateKeyPKCS8PEM(const aKey: TsgcRSAPrivateKey): string;
Importowanie klucza RSA
Zanim powstały te funkcje, TsgcRSAPrivateKey mógł pochodzić wyłącznie z sgcRSA_GenerateKey, co oznaczało, że klucza zapisanego na dysku w ogóle nie dało się użyć. Funkcje importu odczytują oba kontenery klucza prywatnego, PKCS#1 RSAPrivateKey i PKCS#8 PrivateKeyInfo, oraz oba kodowania klucza publicznego, PKCS#1 RSAPublicKey i X.509 SubjectPublicKeyInfo, jako DER albo jako PEM. Zaszyfrowany PKCS#8, forma BEGIN ENCRYPTED PRIVATE KEY, nie jest obsługiwany i zwraca False. Parametry CRT są przeliczane na nowo, gdy plik ich nie zawiera, więc zaimportowany klucz nadal działa z pełną szybkością.
function sgcRSA_ImportPrivateKeyDER(const aDER: TBytes; out aKey: TsgcRSAPrivateKey): Boolean;
function sgcRSA_ImportPrivateKeyPEM(const aPEM: string; out aKey: TsgcRSAPrivateKey): Boolean;
function sgcRSA_ImportPublicKeyDER(const aDER: TBytes; out aKey: TsgcRSAPublicKey): Boolean;
function sgcRSA_ImportPublicKeyPEM(const aPEM: string; out aKey: TsgcRSAPublicKey): Boolean;
var
oKey: TsgcRSAPrivateKey;
oSignature: TBytes;
begin
if sgcRSA_ImportPrivateKeyPEM(Memo1.Text, oKey) then
oSignature := sgcRSA_PSS_Sign(oKey, oData, rhSHA256);
end;
Certyfikat nie jest kluczem publicznym, więc przekaż sgcRSA_ImportPublicKeyDER strukturę SubjectPublicKeyInfo wyjętą z certyfikatu, a nie sam certyfikat. sgcRSA_CheckPrivateKey weryfikuje wewnętrzną spójność czegokolwiek, co pochodzi skądinąd: że n = p*q, że e*d przystaje do 1, oraz że parametry CRT się zgadzają.
Ed25519 i Ed448 (podpisy)
Nonce w EdDSA są wyprowadzane deterministycznie z klucza prywatnego i wiadomości, więc w podpisywanie nie jest zaangażowany żaden generator liczb losowych i nie ma możliwości, aby powtórzony nonce ujawnił klucz, w przeciwieństwie do ECDSA (patrz niżej). sgcCrypto_Ed25519 to prymityw weryfikacji/podpisywania używany przez typ poświadczenia WebAuthn EdDSA w innych miejscach sgcWebSockets; zakłada, że wywołujący zna już 32-bajtowy klucz publiczny (WebAuthn przekazuje go z poświadczenia) i nie udostępnia osobnego wywołania do generowania kluczy ani wyprowadzania klucza publicznego. sgcCrypto_Ed448 to podpisujący ogólnego przeznaczenia, z bezpieczeństwem na poziomie w przybliżeniu 224 bitów wobec 128 dla Ed25519, kosztem większego klucza i podpisu oraz wolniejszego podpisywania.
{ Ed25519: caller supplies the 32-byte seed and, to verify, the 32-byte public key. }
function sgcEd25519_Sign(const aPrivateSeed, aMessage: TBytes): TBytes;
function sgcEd25519_Verify(const aPublicKey, aMessage, aSignature: TBytes): Boolean;
{ Ed448: 57-byte keys, 114-byte signatures, optional context string. }
procedure sgcEd448_GenerateKeyPair(out aPrivateKey, aPublicKey: TBytes);
function sgcEd448_Sign(const aPrivateKey, aMessage, aContext: TBytes): TBytes;
function sgcEd448_Verify(const aPublicKey, aMessage, aSignature, aContext: TBytes): Boolean;
X25519 i X448 (uzgadnianie klucza)
Diffie-Hellman na krzywych Montgomery'ego: obie strony generują parę kluczy, wymieniają klucze publiczne, a każda z nich oblicza ten sam sekret współdzielony na podstawie własnego klucza prywatnego i klucza publicznego drugiej strony. Zawsze przepuszczaj surowy wynik przez KDF (patrz Haszowanie haseł i wyprowadzanie kluczy, HKDF) przed użyciem go do szyfrowania czegokolwiek; surowy wynik ECDH sam w sobie nie nadaje się na klucz. X448 daje bezpieczeństwo na poziomie w przybliżeniu 224 bitów wobec 128 dla X25519, kosztem większego klucza i mniej więcej trzykrotnie większych obliczeń.
function sgcX25519_PublicKey(const aPrivateKey: TBytes): TBytes;
function sgcX25519_SharedSecret(const aPrivateKey, aPeerPublicKey: TBytes; out aSecret: TBytes): Boolean;
function sgcX448_PublicKey(const aPrivateKey: TBytes): TBytes;
function sgcX448_SharedSecret(const aPrivateKey, aPeerPublicKey: TBytes; out aSecret: TBytes): Boolean;
var
oAlicePriv, oBobPriv, oAlicePub, oBobPub, oSecretA, oSecretB: TBytes;
begin
oAlicePriv := sgcRandomBytes(32);
oBobPriv := sgcRandomBytes(32);
oAlicePub := sgcX25519_PublicKey(oAlicePriv);
oBobPub := sgcX25519_PublicKey(oBobPriv);
sgcX25519_SharedSecret(oAlicePriv, oBobPub, oSecretA);
sgcX25519_SharedSecret(oBobPriv, oAlicePub, oSecretB);
{ oSecretA = oSecretB }
end;
SharedSecret zwraca False dla klucza rówieśnika o niskim rzędzie, czyli niewielkiego zbioru kluczy publicznych, które wymuszają, aby sekret współdzielony był zawsze zerowy, niezależnie od Twojego klucza prywatnego. Zawsze sprawdzaj wynik, zamiast bezwarunkowo ufać danym wyjściowym.
ECIES (szyfrowanie sealed box)
Łączy X25519, HKDF-SHA256 i XChaCha20-Poly1305 w jednym wywołaniu, które szyfruje wiadomość do klucza publicznego odbiorcy, bez potrzeby wcześniejszego sekretu współdzielonego. aContext wiąże zapieczętowaną wiadomość z celem lub etapem protokołu, otwieranie z innym kontekstem kończy się niepowodzeniem, co uniemożliwia odtworzenie zapieczętowanej wiadomości z jednej wymiany w innej.
procedure sgcECIES_GenerateKeyPair(out aPrivateKey, aPublicKey: TBytes);
function sgcECIES_Seal(const aRecipientPublicKey, aPlain, aAAD, aContext: TBytes): TBytes;
function sgcECIES_Open(const aPrivateKey, aSealed, aAAD, aContext: TBytes; out aPlain: TBytes): Boolean;
ECDSA / ECDH na nazwanych krzywych
Dwie osobne jednostki obejmują krótkie krzywe Weierstrassa, podzielone według tego, którą rodzinę krzywych implementują:
sgcCrypto_EC obejmuje krzywe NIST (P-256, P-384, P-521) i jest zorientowana na klucze PEM oraz podpisy JWS, w kształcie oczekiwanym przez biblioteki JWT (ES256 i podobne).
sgcCrypto_ECCurves obejmuje secp256k1 (krzywą Bitcoina i Ethereum) oraz krzywe brainpoolP256r1 / P384r1 / P512r1 z RFC 5639, działając bezpośrednio na surowych bajtach klucza i podpisu, a nie na PEM.
TsgcECCurve w sgcCrypto_ECCurves nazywa teraz siedem krzywych zamiast czterech: eccSecp256k1, trzy krzywe Brainpool oraz eccP256, eccP384 i eccP521 dla NIST P-256, P-384 i P-521. Wcześniej trzy krzywe NIST były osiągalne wyłącznie przez API PEM i JWS jednostki sgcCrypto_EC, więc aplikacja trzymająca klucz P-256 jako surowe bajty musiała najpierw zapisać go jako PEM. Poniższe wywołania na surowych kluczach, generowanie, podpisywanie, weryfikacja, ECDH i kompresja, obejmują teraz wszystkie siedem. Sięgaj po sgcCrypto_EC, gdy zadaniem jest JWS, a po sgcCrypto_ECCurves, gdy materiał klucza jest już w postaci bajtów.
Obie jednostki deklarują typ o nazwie TsgcECCurve i procedurę o nazwie sgcEC_GenerateKeyPair, o różnym znaczeniu w każdej jednostce. Nie dodawaj obu jednostek do tej samej klauzuli uses i nie odwołuj się do żadnej z nazw bez kwalifikacji; kwalifikuj nazwą jednostki (sgcCrypto_ECCurves.sgcEC_GenerateKeyPair), jeśli jedna jednostka rzeczywiście potrzebuje obu.
{ sgcCrypto_ECCurves: secp256k1 and Brainpool, RFC 6979 deterministic nonces,
low-S normalized signatures (BIP 62). }
procedure sgcEC_GenerateKeyPair(aCurve: TsgcECCurve; out aPrivateKey, aPublicKey: TBytes);
function sgcECDSA_SignHash(aCurve: TsgcECCurve; const aPrivateKey, aHash: TBytes; aHashKind: TsgcECHash = echSHA256): TBytes;
function sgcECDSA_VerifyHash(aCurve: TsgcECCurve; const aPublicKey, aHash, aSignature: TBytes): Boolean;
function sgcECDH_SharedSecret(aCurve: TsgcECCurve; const aPrivateKey, aPeerPublicKey: TBytes; out aSecret: TBytes): Boolean;
function sgcEC_ValidatePublicKey(aCurve: TsgcECCurve; const aPublicKey: TBytes): Boolean;
function sgcEC_Compress(aCurve: TsgcECCurve; const aPublicKey: TBytes): TBytes;
function sgcEC_Decompress(aCurve: TsgcECCurve; const aCompressed: TBytes): TBytes;
Podpisy ECDSA używają tu deterministycznych nonce z RFC 6979: nonce jest wyprowadzany z klucza prywatnego i wiadomości, zamiast być losowany z generatora liczb losowych, co eliminuje najczęstszą przyczynę utraty klucza prywatnego ECDSA w rzeczywistych wdrożeniach, czyli powtórzony lub częściowo przewidywalny nonce. Podpisy mają normalizację low-S wymaganą przez Bitcoina (BIP 62) i Ethereum; weryfikacja akceptuje obie formy.
Podpisy ECDSA zakodowane w DER
Jeden podpis, dwa kodowania, a wybór niewłaściwego jest jednym z najczęstszych błędów integracji w tym obszarze. Surowa para R || S, każda połowa o rozmiarze pola krzywej, to postać, jaką niosą JOSE i WebAuthn. DER SEQUENCE { r, s } to postać, jaką niosą X.509, CMS i TLS. sgcECDSA_SignHash powyżej wytwarza postać surową, poniższe wywołania wytwarzają i przyjmują postać DER, a oba konwertery tłumaczą między nimi bez ponownego podpisywania czegokolwiek.
{ sgcCrypto_ECCurves: surowe klucze, skrót już obliczony przez wywołującego. }
function sgcECDSA_SignDER(aCurve: TsgcECCurve; const aPrivateKey, aHash: TBytes; aHashKind: TsgcECHash): TBytes; overload;
function sgcECDSA_VerifyDER(aCurve: TsgcECCurve; const aPublicKey, aHash, aDERSignature: TBytes): Boolean; overload;
function sgcECDSA_RawToDER(aCurve: TsgcECCurve; const aRaw: TBytes): TBytes;
function sgcECDSA_DERToRaw(aCurve: TsgcECCurve; const aDER: TBytes; out aRaw: TBytes): Boolean;
{ sgcCrypto_EC: klucze PEM, haszuje samo aData skrótem SHA właściwym dla krzywej. }
function sgcECDSA_SignDER(const aPrivateKeyPEM: string; const aData: TBytes; aCurveBits: Integer): TBytes; overload;
function sgcECDSA_VerifyDER(const aPublicPoint, aData, aDERSignature: TBytes; aCurveBits: Integer): Boolean; overload;
var
oRaw, oDER: TBytes;
begin
oRaw := sgcECDSA_SignHash(eccP256, oPrivateKey, oDigest, echSHA256);
oDER := sgcECDSA_RawToDER(eccP256, oRaw);
end;
aHashKind w wersji na surowych kluczach wybiera wyłącznie HMAC stojący za noncem z RFC 6979, sam niczego dla Ciebie nie haszuje, więc przekazuj sgcEC_DefaultHash(aCurve), gdy nie ma powodu wybierać inaczej. sgcECDSA_RawToDER zwraca pustą tablicę, gdy para nie składa się dokładnie z dwóch połówek o rozmiarze pola, a sgcECDSA_DERToRaw zwraca False przy zniekształconym DER albo przy wartości niepasującej do krzywej, więc obie funkcje można bezpiecznie kierować na niezaufane dane wejściowe.
Eksport i import kluczy EC
Trzy kontenery, a to etykieta PEM odróżnia je od siebie: SEC1 ECPrivateKey to BEGIN EC PRIVATE KEY, PKCS#8 PrivateKeyInfo to BEGIN PRIVATE KEY, a X.509 SubjectPublicKeyInfo to BEGIN PUBLIC KEY. Java i .NET oczekują PKCS#8, OpenSSL i większość narzędzi wiersza poleceń odczytuje którykolwiek. Żadna z postaci prywatnych nie jest zaszyfrowana, więc wynik jest tak samo tajny, jak sam skalar.
function sgcEC_ExportPrivateKeySEC1DER(aCurve: TsgcECCurve; const aPrivateKey, aPublicKey: TBytes): TBytes;
function sgcEC_ExportPrivateKeyPKCS8DER(aCurve: TsgcECCurve; const aPrivateKey, aPublicKey: TBytes): TBytes;
function sgcEC_ExportSubjectPublicKeyInfo(aCurve: TsgcECCurve; const aPublicKey: TBytes): TBytes;
function sgcEC_ExportPrivateKeySEC1PEM(aCurve: TsgcECCurve; const aPrivateKey, aPublicKey: TBytes): string;
function sgcEC_ExportPrivateKeyPKCS8PEM(aCurve: TsgcECCurve; const aPrivateKey, aPublicKey: TBytes): string;
function sgcEC_ExportPublicKeyPEM(aCurve: TsgcECCurve; const aPublicKey: TBytes): string;
function sgcEC_ImportPrivateKeyDER(const aDER: TBytes; out aCurve: TsgcECCurve; out aPrivateKey, aPublicKey: TBytes): Boolean;
function sgcEC_ImportPrivateKeyPEM(const aPEM: string; out aCurve: TsgcECCurve; out aPrivateKey, aPublicKey: TBytes): Boolean;
function sgcEC_ImportPublicKeyDER(const aDER: TBytes; out aCurve: TsgcECCurve; out aPublicKey: TBytes): Boolean;
function sgcEC_ImportPublicKeyPEM(const aPEM: string; out aCurve: TsgcECCurve; out aPublicKey: TBytes): Boolean;
var
oCurve: TsgcECCurve;
oPrivateKey, oPublicKey: TBytes;
begin
if sgcEC_ImportPrivateKeyPEM(Memo1.Text, oCurve, oPrivateKey, oPublicKey) then
Memo2.Text := sgcEC_ExportPrivateKeyPKCS8PEM(oCurve, oPrivateKey, oPublicKey);
end;
Funkcje importu odczytują krzywą z pliku, dlatego aCurve wraca jako parametr out zamiast być przekazywana na wejściu. Krzywa, której jednostka nie implementuje, to niepowodzenie negocjacji, a nie błąd, więc funkcje te zwracają False i nigdy nie zgłaszają wyjątku, co czyni je bezpiecznymi na niezaufanych danych wejściowych. Punkt publiczny jest przeliczany na nowo ze skalara, gdy plik go pomija, i walidowany względem krzywej, zanim zostanie zwrócony. Funkcje eksportu zamiast tego zgłaszają wyjątek, ponieważ materiał klucza niepasujący do krzywej jest błędem wywołującego, a nie danymi wejściowymi.
Kropkowany OID nazwanej krzywej, który trafia do każdego z tych kontenerów, jest dostępny bezpośrednio i to właśnie z niego korzystają wewnętrznie funkcje eksportu i importu:
function sgcEC_CurveOID(aCurve: TsgcECCurve): string;
function sgcEC_CurveFromOID(const aDotted: string; out aCurve: TsgcECCurve): Boolean;
Zawsze waliduj klucz publiczny rówieśnika
Przed użyciem jakiegokolwiek klucza publicznego, którego sam nie wygenerowałeś, czy to do ECDH, weryfikacji ECDSA, czy ECIES, zweryfikuj, że jest to rzeczywiście punkt na krzywej, za pomocą sgcEC_ValidatePublicKey. Punkt, który nie leży na krzywej, może ujawnić bity Twojego własnego klucza prywatnego poprzez arytmetykę użytą do jego przetworzenia.
Podpisy Schnorra (BIP-340)
Wyłącznie secp256k1, i to właśnie z tego korzystają Taproot, Nostr i Lightning. Nie jest to wprost zamiennik ECDSA powyżej, ponieważ różnią je dwie rzeczy. Klucze publiczne są x-only, 32 bajty, z domyślnie parzystą współrzędną y, więc klucz BIP-340 nie jest wymienny z 33-bajtową postacią skompresowaną ani 65-bajtową postacią nieskompresowaną, które wytwarza reszta tej strony. Ponadto wyzwanie haszuje klucz publiczny razem z noncem i wiadomością, co wiąże podpis z kluczem w sposób, w jaki nie wiąże go podpis ECDSA.
function sgcSchnorr_PublicKey(const aPrivateKey: TBytes): TBytes;
function sgcSchnorr_Sign(const aPrivateKey, aMessage, aAuxRand: TBytes): TBytes;
function sgcSchnorr_Verify(const aPublicKey, aMessage, aSignature: TBytes): Boolean;
function sgcSchnorr_TaggedHash(const aTag: string; const aData: TBytes): TBytes;
var
oPrivateKey, oPublicKey, oSignature: TBytes;
begin
oPrivateKey := sgcRandomBytes(32);
oPublicKey := sgcSchnorr_PublicKey(oPrivateKey); { 32 bajty, x-only }
oSignature := sgcSchnorr_Sign(oPrivateKey, oMessage, sgcRandomBytes(32));
if sgcSchnorr_Verify(oPublicKey, oMessage, oSignature) then
{ valid };
end;
aAuxRand to 32 bajty pomocniczej losowości wmieszanej w wyprowadzanie nonce. To dodatkowa warstwa obrony przed atakami przez wstrzykiwanie błędów, a nie sam nonce: schemat pozostaje bezpieczny nawet przy samych zerach, a pusta tablica jest traktowana jak 32 zerowe bajty, dzięki czemu wektory testowe BIP-340 odtwarzają się dokładnie. Rzeczywiste użycie powinno przekazywać świeżą losowość. sgcSchnorr_TaggedHash jest udostępniony, ponieważ BIP-341 i BIP-342 budują na tej samej konstrukcji kolejne tagi.