| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120 |
- using System.Security.Cryptography;
- using System.Text;
- namespace Infrastructure.Kyc.Nice;
- /// <summary>
- /// <see cref="NiceCrypto"/> 의 invariants self-check.
- ///
- /// dev 환경 부팅 시 1회 실행해서 PBKDF2 출력 길이, AES-GCM 라운드트립, HMAC 라운드트립이
- /// 깨지지 않았음을 확인. 라이브러리/런타임 업데이트로 silently 동작이 바뀌는 회귀를 방지.
- ///
- /// 진짜 NICE 호환성 검증은 별도 phase (xUnit + NICE 공식 PHP/Node.js sample 의 골든벡터) 에서.
- /// </summary>
- public static class NiceCryptoInvariants
- {
- /// <summary>
- /// 실패 시 <see cref="CryptographicException"/> throw.
- /// </summary>
- public static void EnsureOrThrow()
- {
- EnsureBase64UrlRoundTrip();
- EnsureDeriveKeysShape();
- EnsureHmacRoundTrip();
- EnsureAesGcmRoundTrip();
- }
- private static void EnsureBase64UrlRoundTrip()
- {
- // 패딩 길이 0/1/2 byte 모두 라운드트립
- var samples = new[]
- {
- new byte[] { 0x00 },
- new byte[] { 0x01, 0x02 },
- new byte[] { 0xFF, 0xFE, 0xFD },
- Encoding.UTF8.GetBytes("hello world")
- };
- foreach (var s in samples)
- {
- var encoded = NiceCrypto.Base64UrlEncode(s);
- if (encoded.Contains('+') || encoded.Contains('/') || encoded.EndsWith('='))
- {
- throw new CryptographicException($"Base64UrlEncode 가 URL-safe + no-padding 규칙을 위반: '{encoded}'");
- }
- var decoded = NiceCrypto.Base64UrlDecode(encoded);
- if (!decoded.AsSpan().SequenceEqual(s))
- {
- throw new CryptographicException("Base64Url 라운드트립 실패.");
- }
- }
- }
- private static void EnsureDeriveKeysShape()
- {
- // 임의 vector — NICE 호환 vector 아님. 길이/결정성만 검증.
- var (aesKey, hmacKey) = NiceCrypto.DeriveKeys("ticket-self-check", "transaction-id-self-check", 1000);
- if (aesKey.Length != 32)
- {
- throw new CryptographicException($"DeriveKeys: AES key length = {aesKey.Length} (expected 32).");
- }
- if (hmacKey.Length != 32)
- {
- throw new CryptographicException($"DeriveKeys: HMAC key length = {hmacKey.Length} (expected 32).");
- }
- // 같은 입력 → 같은 출력 (deterministic)
- var (aesKey2, hmacKey2) = NiceCrypto.DeriveKeys("ticket-self-check", "transaction-id-self-check", 1000);
- if (!aesKey.AsSpan().SequenceEqual(aesKey2) || !hmacKey.AsSpan().SequenceEqual(hmacKey2))
- {
- throw new CryptographicException("DeriveKeys 가 결정론적이지 않습니다.");
- }
- }
- private static void EnsureHmacRoundTrip()
- {
- var hmacKey = new byte[32];
- RandomNumberGenerator.Fill(hmacKey);
- const string encData = "AbCdEfGhIjKlMnOpQrStUvWxYz0123456789";
- var integrity = NiceCrypto.ComputeIntegrity(hmacKey, encData);
- if (!NiceCrypto.VerifyIntegrity(hmacKey, encData, integrity))
- {
- throw new CryptographicException("HMAC ComputeIntegrity ↔ VerifyIntegrity 라운드트립 실패.");
- }
- // 다른 키로는 fail
- var otherKey = new byte[32];
- RandomNumberGenerator.Fill(otherKey);
- if (NiceCrypto.VerifyIntegrity(otherKey, encData, integrity))
- {
- throw new CryptographicException("HMAC VerifyIntegrity 가 잘못된 키를 통과시켰습니다.");
- }
- }
- private static void EnsureAesGcmRoundTrip()
- {
- var aesKey = new byte[32];
- RandomNumberGenerator.Fill(aesKey);
- const string plaintext = "{\"name\":\"홍길동\",\"birthdate\":\"19800101\",\"gender\":\"1\"}";
- // NICE 와 동일한 16 byte IV + ciphertext+tag 레이아웃 (BouncyCastle 기반).
- var encData = NiceCrypto.EncryptAesGcm(aesKey, plaintext);
- var decrypted = NiceCrypto.DecryptAesGcm(aesKey, encData);
- if (decrypted != plaintext)
- {
- throw new CryptographicException($"AES-GCM 라운드트립 실패. expected='{plaintext}', got='{decrypted}'");
- }
- // 위변조 검출: 임의 1 byte 변경 시 throw
- var tamperedBytes = NiceCrypto.Base64UrlDecode(encData);
- tamperedBytes[tamperedBytes.Length - 1] ^= 0x01;
- var tampered = NiceCrypto.Base64UrlEncode(tamperedBytes);
- try
- {
- NiceCrypto.DecryptAesGcm(aesKey, tampered);
- throw new CryptographicException("AES-GCM 위변조된 데이터를 무사 통과시켰습니다.");
- }
- catch (Org.BouncyCastle.Crypto.InvalidCipherTextException)
- {
- // 정상 — GCM tag 가 위변조 감지
- }
- }
- }
|