@@ -14,6 +14,14 @@ namespace Ed25519.Tests;
1414/// </summary>
1515public class Ed25519Tests
1616{
17+ private static readonly byte [ ] ScalarOrderL =
18+ [
19+ 0xED , 0xD3 , 0xF5 , 0x5C , 0x1A , 0x63 , 0x12 , 0x58 ,
20+ 0xD6 , 0x9C , 0xF7 , 0xA2 , 0xDE , 0xF9 , 0xDE , 0x14 ,
21+ 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ,
22+ 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x10
23+ ] ;
24+
1725 // RFC 8032 Section 7.1 - TEST 1
1826 // SECRET KEY: 9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60
1927 // PUBLIC KEY: d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a
@@ -214,6 +222,49 @@ public void SignatureWithNonCanonicalS_ShouldNotVerify()
214222 Assert . False ( Ed25519 . Verify ( signature , message , publicKey ) ) ;
215223 }
216224
225+ [ Fact ]
226+ public void Verify_WithSExactlyL_ShouldReturnFalse ( )
227+ {
228+ byte [ ] lowOrderPublicKey = new byte [ 32 ] ;
229+ lowOrderPublicKey [ 0 ] = 0x01 ; // Edwards identity
230+
231+ byte [ ] signature = new byte [ 64 ] ;
232+ signature [ 0 ] = 0x01 ; // Encoded identity for R
233+ ScalarOrderL . CopyTo ( signature . AsSpan ( 32 , 32 ) ) ;
234+
235+ Assert . False ( Ed25519 . Verify ( signature , "m"u8 , lowOrderPublicKey ) ) ;
236+ }
237+
238+ [ Fact ]
239+ public void Verify_WithSGreaterThanL_ShouldReturnFalse ( )
240+ {
241+ byte [ ] lowOrderPublicKey = new byte [ 32 ] ;
242+ lowOrderPublicKey [ 0 ] = 0x01 ; // Edwards identity
243+
244+ byte [ ] signature = new byte [ 64 ] ;
245+ signature [ 0 ] = 0x01 ; // Encoded identity for R
246+ ScalarOrderL . CopyTo ( signature . AsSpan ( 32 , 32 ) ) ;
247+ signature [ 32 ] ++ ; // S = L + 1
248+
249+ Assert . False ( Ed25519 . Verify ( signature , "m"u8 , lowOrderPublicKey ) ) ;
250+ }
251+
252+ [ Fact ]
253+ public void Verify_WithValidSignatureMutatedByAddingLToS_ShouldReturnFalse ( )
254+ {
255+ var ( publicKey , privateKey , _) = Ed25519 . GenerateKeypair ( ) ;
256+ byte [ ] message = "malleability-check"u8 . ToArray ( ) ;
257+ byte [ ] signature = Ed25519 . Sign ( message , publicKey , privateKey ) ;
258+
259+ byte [ ] mutated = signature . ToArray ( ) ;
260+ AddLittleEndian ( mutated . AsSpan ( 32 , 32 ) , ScalarOrderL ) ;
261+
262+ if ( ( mutated [ 63 ] & 0b1110_0000 ) == 0 )
263+ {
264+ Assert . False ( Ed25519 . Verify ( mutated , message , publicKey ) ) ;
265+ }
266+ }
267+
217268 [ Fact ]
218269 public void Verify_WithInvalidPublicKeyEncoding_ShouldReturnFalse ( )
219270 {
@@ -238,6 +289,23 @@ public void Verify_WithLowOrderPublicKey_ShouldReturnFalse()
238289 Assert . False ( Ed25519 . Verify ( signature , message , lowOrderPublicKey ) ) ;
239290 }
240291
292+ [ Fact ]
293+ public void Verify_WithOversizedInputs_ShouldReturnFalse ( )
294+ {
295+ var ( publicKey , privateKey , _) = Ed25519 . GenerateKeypair ( ) ;
296+ byte [ ] message = "oversize"u8 . ToArray ( ) ;
297+ byte [ ] signature = Ed25519 . Sign ( message , publicKey , privateKey ) ;
298+
299+ byte [ ] oversizedSignature = new byte [ 65 ] ;
300+ signature . CopyTo ( oversizedSignature , 0 ) ;
301+
302+ byte [ ] oversizedPublicKey = new byte [ 33 ] ;
303+ publicKey . CopyTo ( oversizedPublicKey , 0 ) ;
304+
305+ Assert . False ( Ed25519 . Verify ( oversizedSignature , message , publicKey ) ) ;
306+ Assert . False ( Ed25519 . Verify ( signature , message , oversizedPublicKey ) ) ;
307+ }
308+
241309 [ Fact ]
242310 public void SignOverload_WithAndWithoutPublicKey_ShouldMatch ( )
243311 {
@@ -255,6 +323,38 @@ public void SignOverload_WithAndWithoutPublicKey_ShouldMatch()
255323 Assert . True ( Ed25519 . Verify ( sig1 , message , publicKey ) ) ;
256324 }
257325
326+ [ Fact ]
327+ public void CreateKeypair_WithUndersizedOrOversizedInputs_ShouldThrow ( )
328+ {
329+ byte [ ] seed = new byte [ Ed25519 . SeedSize ] ;
330+ byte [ ] publicKey = new byte [ Ed25519 . PublicKeySize ] ;
331+ byte [ ] privateKey = new byte [ Ed25519 . PrivateKeySize ] ;
332+
333+ Assert . Throws < ArgumentException > ( ( ) => Ed25519 . CreateKeypair ( publicKey . AsSpan ( 0 , 31 ) , privateKey , seed ) ) ;
334+ Assert . Throws < ArgumentException > ( ( ) => Ed25519 . CreateKeypair ( new byte [ 33 ] , privateKey , seed ) ) ;
335+ Assert . Throws < ArgumentException > ( ( ) => Ed25519 . CreateKeypair ( publicKey , privateKey . AsSpan ( 0 , 63 ) , seed ) ) ;
336+ Assert . Throws < ArgumentException > ( ( ) => Ed25519 . CreateKeypair ( publicKey , new byte [ 65 ] , seed ) ) ;
337+ Assert . Throws < ArgumentException > ( ( ) => Ed25519 . CreateKeypair ( publicKey , privateKey , seed . AsSpan ( 0 , 31 ) ) ) ;
338+ Assert . Throws < ArgumentException > ( ( ) => Ed25519 . CreateKeypair ( publicKey , privateKey , new byte [ 33 ] ) ) ;
339+ }
340+
341+ [ Fact ]
342+ public void Sign_WithUndersizedOrOversizedInputs_ShouldThrow ( )
343+ {
344+ var ( publicKey , privateKey , _) = Ed25519 . GenerateKeypair ( ) ;
345+ byte [ ] message = "size-check-sign"u8 . ToArray ( ) ;
346+
347+ Assert . Throws < ArgumentException > ( ( ) => Ed25519 . Sign ( new byte [ 63 ] , message , publicKey , privateKey ) ) ;
348+ Assert . Throws < ArgumentException > ( ( ) => Ed25519 . Sign ( new byte [ 65 ] , message , publicKey , privateKey ) ) ;
349+ Assert . Throws < ArgumentException > ( ( ) => Ed25519 . Sign ( new byte [ 64 ] , message , publicKey . AsSpan ( 0 , 31 ) , privateKey ) ) ;
350+ Assert . Throws < ArgumentException > ( ( ) => Ed25519 . Sign ( new byte [ 64 ] , message , new byte [ 33 ] , privateKey ) ) ;
351+ Assert . Throws < ArgumentException > ( ( ) => Ed25519 . Sign ( new byte [ 64 ] , message , publicKey , privateKey . AsSpan ( 0 , 63 ) ) ) ;
352+ Assert . Throws < ArgumentException > ( ( ) => Ed25519 . Sign ( new byte [ 64 ] , message , publicKey , new byte [ 65 ] ) ) ;
353+
354+ Assert . Throws < ArgumentException > ( ( ) => Ed25519 . Sign ( new byte [ 64 ] , message , privateKey . AsSpan ( 0 , 63 ) ) ) ;
355+ Assert . Throws < ArgumentException > ( ( ) => Ed25519 . Sign ( new byte [ 64 ] , message , new byte [ 65 ] ) ) ;
356+ }
357+
258358 [ Fact ]
259359 public void DeterministicSignatures_SameInputsSameSignature ( )
260360 {
@@ -484,6 +584,54 @@ public void EncryptedPkcs8Pem_RoundTrip_DecryptAndRecoverSeed()
484584 Assert . Equal ( seed , recoveredSeed ) ;
485585 }
486586
587+ [ Fact ]
588+ public void ExportEncryptedPrivateKeyPem_ObsoleteOverload_ShouldMatchNewOverload ( )
589+ {
590+ var ( publicKey , _, seed ) = Ed25519 . GenerateKeypair ( ) ;
591+ const string password = "migration-check-password" ;
592+
593+ #pragma warning disable CS0618
594+ string oldPem = Pkcs . ExportEncryptedPrivateKeyPem ( seed , publicKey , password , iterations : 1_000 ) ;
595+ #pragma warning restore CS0618
596+ string newPem = Pkcs . ExportEncryptedPrivateKeyPem ( seed , password , iterations : 1_000 ) ;
597+
598+ byte [ ] oldDer = Pkcs . DecodePem ( oldPem , "ENCRYPTED PRIVATE KEY" ) ;
599+ byte [ ] newDer = Pkcs . DecodePem ( newPem , "ENCRYPTED PRIVATE KEY" ) ;
600+
601+ var oldInfo = Pkcs8PrivateKeyInfo . DecryptAndDecode ( password , oldDer , out _ ) ;
602+ var newInfo = Pkcs8PrivateKeyInfo . DecryptAndDecode ( password , newDer , out _ ) ;
603+ Assert . Equal ( Pkcs . DecodePkcs8PrivateKey ( oldInfo . Encode ( ) ) , Pkcs . DecodePkcs8PrivateKey ( newInfo . Encode ( ) ) ) ;
604+ }
605+
606+ [ Fact ]
607+ public void VerifyPkcs10CertificationRequest_WithTamperedSignature_ShouldReturnFalse ( )
608+ {
609+ var ( publicKey , privateKey , _) = Ed25519 . GenerateKeypair ( ) ;
610+ byte [ ] subjectNameDer = new X500DistinguishedName ( "CN=tamper-check" ) . RawData ;
611+ byte [ ] csrDer = Pkcs . EncodePkcs10CertificationRequest ( subjectNameDer , publicKey , privateKey ) ;
612+
613+ byte [ ] tampered = csrDer . ToArray ( ) ;
614+ tampered [ ^ 1 ] ^= 0x01 ;
615+
616+ Assert . False ( Pkcs . VerifyPkcs10CertificationRequest ( tampered , out _ , out _ ) ) ;
617+ }
618+
619+ [ Fact ]
620+ public void VerifyPkcs10CertificationRequest_WithWrongAlgorithmOid_ShouldReturnFalse ( )
621+ {
622+ var ( publicKey , privateKey , _) = Ed25519 . GenerateKeypair ( ) ;
623+ byte [ ] subjectNameDer = new X500DistinguishedName ( "CN=oid-check" ) . RawData ;
624+ byte [ ] csrDer = Pkcs . EncodePkcs10CertificationRequest ( subjectNameDer , publicKey , privateKey ) ;
625+
626+ byte [ ] tampered = csrDer . ToArray ( ) ;
627+ // OID encoding in CSR: 06 03 2B 65 70. Corrupt payload byte.
628+ int oidIndex = Array . IndexOf ( tampered , ( byte ) 0x06 ) ;
629+ Assert . True ( oidIndex >= 0 ) ;
630+ tampered [ oidIndex + 2 ] ^= 0x01 ;
631+
632+ Assert . False ( Pkcs . VerifyPkcs10CertificationRequest ( tampered , out _ , out _ ) ) ;
633+ }
634+
487635 [ Fact ]
488636 public void Pkcs10Csr_RoundTrip_VerifyAndExtract ( )
489637 {
@@ -551,4 +699,17 @@ private static byte[] EncodeWithNonMinimalOuterLength(ReadOnlySpan<byte> der)
551699 der [ 2 ..] . CopyTo ( nonMinimal . AsSpan ( 3 ) ) ;
552700 return nonMinimal ;
553701 }
702+
703+ private static void AddLittleEndian ( Span < byte > destination , ReadOnlySpan < byte > addend )
704+ {
705+ Assert . Equal ( destination . Length , addend . Length ) ;
706+
707+ int carry = 0 ;
708+ for ( int i = 0 ; i < destination . Length ; i ++ )
709+ {
710+ int sum = destination [ i ] + addend [ i ] + carry ;
711+ destination [ i ] = ( byte ) sum ;
712+ carry = sum >> 8 ;
713+ }
714+ }
554715}
0 commit comments