앱 보안 인증 시스템 기술 명세
1. 개요
이 문서는 클라이언트 앱의 무결성 검증 및 안전한 인증을 위한 앱 보안 인증 시스템의 기술적 명세를 제공합니다. 주요 내용은 다음과 같습니다:
- 디바이스 ID(Device ID): 클라이언트 장치를 고유하게 식별하고 인증하기 위한 보안 메커니즘
- 앱 무결성 검증: 클라이언트 앱이 변조되지 않았는지 확인하는 프로세스
- RSA 기반 챌린지-응답 인증: 앱과 서버 간 안전한 통신을 위한 비대칭 암호화 인증 메커니즘
- 암호화/복호화 프로세스: Device ID 및 관련 정보의 안전한 전송과 저장을 위한 암호화 방식
이 문서는 앱 보안 인증 시스템의 전체적인 아키텍처와 흐름, 구현 방법을 설명하며, 앱 토큰 발급 과정과의 연계 방식도 다룹니다. 앱 토큰에 대한 상세 내용은 앱 토큰 기술 명세를 참조하세요.
2. Device ID 형식
2.1 기본 구조
interface DeviceIdentifier {
uuid: string; // 장치 고유 식별자
platform: string; // 플랫폼 정보 (iOS, Android, web)
version: string; // 클라이언트 앱 버전
timestamp: number; // 생성 시간 (Unix timestamp)
}
2.2 해싱 및 직렬화 형식
Device ID는 보안 및 관리 목적으로 해싱 후 저장 및 전송됩니다:
// 디바이스 정보 직렬화
const deviceIdString = JSON.stringify(deviceIdentifier);
// SHA-256 해싱 후 16진수(hex) 문자열로 인코딩
const deviceIdHash = crypto.createHash('sha256')
.update(deviceIdString)
.digest('hex');
해싱된 Device ID를 사용하는 주요 장점:
- 고정 길이: 해시는 원본 데이터 크기와 관계없이 항상 고정된 길이(SHA-256 후 hex 인코딩 시 64자)로 생성됩니다.
- 데이터베이스 인덱싱: 고정 길이 문자열이므로 데이터베이스 인덱싱 효율이 향상됩니다.
- 충돌 방지: 암호학적 해시 함수는 충돌 가능성이 극히 낮아 고유 식별자로 적합합니다.
- 일관성: 클라이언트와 서버 간에 동일한 해싱 알고리즘(SHA-256)과 인코딩 방식(hex)을 사용하여 검증을 수행합니다.
- 디바이스 정보 은닉: 원본 디바이스 정보가 직접 노출되지 않습니다.
주의: 현재 구현은 hex 인코딩을 사용합니다. 만약 base64url 인코딩(digest('base64url'))을 사용하려면 클라이언트와 서버 구현 모두 일관되게 수정해야 합니다.
보안을 더 강화하기 위해 솔트(salt)를 추가할 수도 있습니다:
// 솔트 추가 해싱 (앱 시크릿을 솔트로 사용)
const deviceIdHashWithSalt = crypto.createHash('sha256')
.update(`${deviceIdString}:${APP_SECRET}`)
.digest('base64url');
3. RSA 기반 챌린지-응답(Challenge-Response) 인증 메커니즘
이 장에서는 앱의 무결성 검증을 위한 RSA 기반 챌린지-응답 메커니즘을 설명합니다.
3.1 개요
챌린지-응답 메커니즘은 서버가 클라이언트에게 랜덤 챌린지를 제공하고, 클라이언트가 서버의 공개키로 이를 암호화하여 응답을 생성하는 방식입니다. 서버는 자신의 개인키로 이 응답을 복호화하여 무결성을 증명합니다. 이 메커니즘을 통해 다음을 확인할 수 있습니다:
- 클라이언트가 정당한 앱인지 (변조되지 않았는지)
- 디바이스 ID가 유효한지
- 실시간 응답인지 (재생 공격 방지)
3.2 프로세스 흐름
챌린지-응답 인증 프로세스는 다음과 같은 순서로 진행됩니다:
- 챌린지 요청: 클라이언트가 자신의 Device ID와 함께 서버에 챌린지를 요청합니다.
- 챌린지 생성: 서버는 랜덤 챌린지와 논스(nonce)를 생성하고 클라이언트에게 반환합니다.
- 응답 생성: 클라이언트는 서버의 공개키를 사용하여 RSA 방식으로 챌린지를 암호화합니다.
- 응답 검증: 서버는 개인키로 클라이언트의 응답을 복호화하고 유효성을 검증합니다.
- 토큰 발급: 검증이 성공하면 서버는 앱 토큰을 발급합니다.
3.3 RSA 기반 챌린지-응답 상세
3.4 RSA 암호화/복호화 구현
클라이언트 측 (Swift) - 암호화
// CryptoHelper.swift
// RSA 공개키 로드
static func loadServerPublicKey() throws -> SecKey {
// PEM 형식 키에서 헤더/푸터 제거 및 Base64 디코딩
let publicKeyString = serverPublicKey
.replacingOccurrences(of: "-----BEGIN PUBLIC KEY-----", with: "")
.replacingOccurrences(of: "-----END PUBLIC KEY-----", with: "")
.replacingOccurrences(of: "\n", with: "")
guard let keyData = Data(base64Encoded: publicKeyString) else {
throw ErrorType.publicKeyLoadFailed
}
// X.509 SubjectPublicKeyInfo 형식으로 변환
let attributes: [String: Any] = [
kSecAttrKeyType as String: kSecAttrKeyTypeRSA,
kSecAttrKeyClass as String: kSecAttrKeyClassPublic,
kSecAttrKeySizeInBits as String: 2048
]
guard let publicKey = SecKeyCreateWithData(keyData as CFData, attributes as CFDictionary, nil) else {
throw ErrorType.publicKeyLoadFailed
}
return publicKey
}
// RSA 암호화
static func encryptRSA(_ input: String) throws -> String {
guard let inputData = input.data(using: .utf8) else {
throw ErrorType.invalidDataFormat
}
let publicKey = try loadServerPublicKey()
var error: Unmanaged<CFError>?
guard let encryptedData = SecKeyCreateEncryptedData(
publicKey,
.rsaEncryptionPKCS1,
inputData as CFData,
&error
) as Data? else {
throw error?.takeRetainedValue() as? Error ?? ErrorType.rsaEncryptionFailed
}
return encryptedData.base64EncodedString()
}
서버 측 (Node.js) - 복호화
// 개인키로 암호화된 챌린지를 복호화합니다.
private decryptChallenge(encryptedChallenge: string): string {
// Base64 디코딩
const encryptedBuffer = Buffer.from(encryptedChallenge, 'base64');
// RSA 복호화
const decrypted = privateDecrypt(
{
key: this.privateKey,
padding: constants.RSA_PKCS1_PADDING,
},
encryptedBuffer
);
// UTF-8 문자열로 변환
return decrypted.toString('utf8');
}
이 메커니즘의 핵심 보안 특성:
- 비대칭 암호화: 서버만이 개인키를 보유하므로, 암호화된 챌린지를 복호화할 수 있는 유일한 주체
- 중간자 공격 방지: 클라이언트가 서버의 인증된 공개키를 가지고 있어 위조 불가능
- 기기 바인딩: 키는 특정 디바이스 ID에 바인딩됨
- 요청 고유성: 매 요청마다 고유한 논스 사용
주의: 위 코드는 실제 구현 예시입니다. 서버 측 구현은 다음 문서를 참조하십시오:
- 백엔드: 앱 인증 시스템 구현 가이드
- iOS: 앱 인증 시스템 구현 가이드의 클라이언트 구현 섹션 참조
- Android: 앱 인증 시스템 구현 가이드의 클라이언트 구현 섹션 참조
4. 앱 무결성 검증 및 인증
이 장에서는 Device ID의 검증과 앱 무결성 확인을 통한 앱 토큰 발급 과정을 설명합니다. 구체적인 앱 토큰 발급 방법 및 구현에 대한 상세 내용은 앱 토큰 기술 명세를 참조하세요.
4.1 Device ID 검증
서버는 클라이언트로부터 받은 Device ID를 다음과 같이 검증합니다:
- 원본 데이터 검증: 디바이스 정보를 재해싱하여 일치 여부 확인
- 형식 검증: 유효한 SHA-256 해시 형식인지 확인 (64자 16진수)
- 필수 필드 확인: UUID, 플랫폼, 버전 등 필수 정보 존재 여부 확인
// 함수 구현 참조: libs/feature/auth/src/lib/domain/services/device-identifier.service.ts 파일의 validateDeviceId 메서드
function validateDeviceIdHash(hashedDeviceId: string, deviceInfo: Record<string, any>): boolean {
// 1. 해시 형식 검증 (64자 16진수)
if (!/^[0-9a-f]{64}$/i.test(hashedDeviceId)) {
return false;
}
// 2. 필수 필드 검증
if (!deviceInfo.uuid || !deviceInfo.platform || !deviceInfo.version) {
return false;
}
// 3. 해시 일치 검증
const computedHash = hashDeviceId(deviceInfo);
return computedHash === hashedDeviceId;
}
4.2 챌린지 검증
서버는 클라이언트가 제공한 암호화된 챌린지를 다음 단계로 검증합니다:
- 캐시 확인: 논스에 해당하는 원본 챌린지 조회
- 적시성 확인: 챌린지가 유효 시간 내에 응답되었는지 확인
- RSA 복호화: 서버의 개인키로 암호화된 챌린지 복호화
- 일치 확인: 복호화된 챌린지와 원본 챌린지 비교
// 함수 구현 참조: libs/feature/auth/src/lib/domain/services/challenge-response.service.ts 파일의 validateResponse 메서드
async function verifyChallenge(
deviceId: string,
encryptedChallenge: string,
nonce: string
): Promise<boolean> {
// 1. 캐시에서 원본 챌린지 검색
const cachedData = await cacheManager.get(`challenge:${deviceId}:${nonce}`);
if (!cachedData) {
return false; // 캐시된 챌린지 없음 (만료 또는 유효하지 않은 논스)
}
// 2. 캐시에서 가져온 챌린지
const { challenge } = cachedData;
// 3. 검증 후 캐시 삭제 (일회성)
await cacheManager.del(`challenge:${deviceId}:${nonce}`);
// 4. 암호화된 챌린지 복호화
const decryptedChallenge = decryptChallenge(encryptedChallenge);
// 5. 원본과 복호화된 챌린지 비교
return challenge === decryptedChallenge;
}
4.3 앱 토큰 발급
챌린지 검증이 성공하면 서버는 앱 토큰을 발급합니다. 앱 토큰 발급과 관련된 상세 내용은 앱 토큰 구현 가이드를 참조하세요.
// 함수 구현 참조: libs/feature/auth/src/lib/domain/services/app-token.service.ts 파일의 generateAppToken 메서드
function generateAppToken(deviceId: DeviceIdentifier): string {
// 토큰 페이로드 생성
const payload = {
sub: deviceId.getValue(),
type: 'app_token',
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + APP_TOKEN_EXPIRATION
};
// JWT 토큰 생성
return jwtService.sign(payload, {
secret: APP_TOKEN_SECRET
});
}
5. 앱 보안 인증 API
앱 보안 인증 시스템은 다음 API 엔드포인트를 통해 구현됩니다:
5.1 챌린지 요청 및 완료 API
챌린지 요청:
POST /de/v1/auth/app/challenge
챌린지 완료 및 앱 토큰 발급:
POST /de/v1/auth/app/complete-challenge
6. 변경 이력
| 버전 | 날짜 | 작성자 | 변경 내용 |
|---|---|---|---|
| 0.1.0 | 2025-03-15 | bok@weltcorp.com | 최초 작성 |
| 0.2.0 | 2025-03-20 | bok@weltcorp.com | ChaCha20 암호화 관련 내용 추가 |
| 0.3.0 | 2025-03-21 | bok@weltcorp.com | GCP Cloud KMS 기반 키 관리 및 모바일 앱 인증 방법 추가 |
| 0.4.0 | 2025-03-25 | bok@weltcorp.com | DCAppAttestService/Play Integrity API 대신 시간 기반 OTP 챌린지 인증 메커니즘 추가 |
| 0.5.0 | 2025-04-15 | bok@weltcorp.com | 대칭 키 기반 메커니즘에서 RSA 비대칭 암호화 방식으로 변경 |