서명과 개인키 처리
SCOPE Connect 서버는 개인키를 수집하거나 저장하지 않습니다. WalletKit의 세션 API도 개인키를 생성·보관하지 않고, 서명이 필요할 때 지갑 앱이 제공한 Signer를 호출합니다. 개인키 생성·보관·사용자 인증·백업·복구 정책은 지갑 제공사가 책임집니다.
다만 WalletKit이 제공하는 InMemorySigner와 Secp256k1Signer를 사용하면 개인키 바이트가 지갑 앱 프로세스 메모리에서 처리됩니다. InMemorySigner는 테스트·데모에만 사용하고, 실제 지갑은 자체 보안 구조에 맞게 Signer를 구현합니다.
승인된 서명 요청이 이 지점까지 오는 과정은 세션 연결과 승인 단계에서 다룹니다.
Signer 인터페이스
connect()에 넘긴 Signer가 서명 계정과 실제 서명 연산을 제공합니다.
- Kotlin (Android · JVM)
- Swift (iOS)
public interface Signer {
/** 서명자 EVM 주소. 0x 접두사가 있는 소문자, 체크섬 없음 */
public val address: String
/** 32바이트 EIP-712 서명 해시를 복구 가능한 서명으로 반환 */
public suspend fun signRecoverable(digest: ByteArray): EthSignature
}
// NSObject를 상속한 클래스로 구현합니다.
protocol Signer {
// 서명자 EVM 주소. 0x 접두사가 있는 소문자, 체크섬 없음
var address: String { get }
// 32바이트 EIP-712 서명 해시를 복구 가능한 서명으로 반환
func signRecoverable(digest: KotlinByteArray) async throws -> EthSignature
}
| 멤버 | 설명 |
|---|---|
address | 세션 연결 단계에서 dApp에 전송되는 서명자 EVM 주소 |
signRecoverable | 라이브러리가 계산한 32바이트 EIP-712 서명 해시에 서명 |
라이브러리는 요청의 EIP-712 구조화 데이터로부터 32바이트 서명 해시(digest)를 계산한 뒤 signRecoverable(digest)를 호출합니다. 지갑 앱은 이 해시에 서명해 65바이트 복구 가능한 서명을 반환하면 되며, EIP-712 인코딩을 직접 구현할 필요가 없습니다. signRecoverable은 Kotlin 비동기 함수인 suspend 함수이므로 생체 인증처럼 사용자 상호작용이 필요한 서명도 기다릴 수 있습니다.
실제 지갑 앱의 개인키 처리
EVM의 secp256k1 개인키를 처리하는 방법은 지갑 앱의 보안 구조와 플랫폼 기능에 따라 달라집니다. WalletKit은 특정 저장 방식을 강제하지 않습니다. 개인키 바이트를 직접 받는 서명 함수를 사용하는 구현은 키가 앱 메모리에 머무는 시간을 최소화해야 합니다.
- 플랫폼 보안 기능과 지갑 자체 암호화 정책을 조합해 개인키 접근을 제한합니다.
signRecoverable호출 직전에 사용자 인증을 거쳐 필요한 개인키 바이트만 메모리에 불러옵니다.- 서명이 끝나면 임시 바이트 배열을 즉시 덮어쓰고 참조를 해제합니다.
- 키 생성·백업·복구 정책은 지갑 제공사가 소유하며 WalletKit은 관여하지 않습니다.
- Kotlin (Android · JVM)
- Swift (iOS)
// 지갑의 보호된 키 저장 방식과 연동하는 Signer의 형태 (개념 예시)
class WalletSigner(
override val address: String,
private val loadPrivateKey: suspend () -> ByteArray,
) : Signer {
override suspend fun signRecoverable(digest: ByteArray): EthSignature {
val privateKey = loadPrivateKey()
try {
return Secp256k1Signer.signRecoverable(digest, privateKey)
} finally {
privateKey.fill(0) // 사용 후 즉시 덮어쓰기
}
}
}
// 지갑의 보호된 키 저장 방식과 연동하는 Signer의 형태 (개념 예시)
// Kotlin 인터페이스는 Swift에 프로토콜로 노출됩니다 — NSObject를 상속한 클래스로 구현합니다.
final class WalletSigner: NSObject, Signer {
let address: String
private let loadPrivateKey: () async throws -> KotlinByteArray
init(address: String, loadPrivateKey: @escaping () async throws -> KotlinByteArray) {
self.address = address
self.loadPrivateKey = loadPrivateKey
}
// Kotlin ByteArray는 Swift에 KotlinByteArray로 노출됩니다
func signRecoverable(digest: KotlinByteArray) async throws -> EthSignature {
let privateKey = try await loadPrivateKey()
defer {
// 사용 후 즉시 덮어쓰기
for i in 0..<privateKey.size { privateKey.set(index: i, value: 0) }
}
return Secp256k1Signer.shared.signRecoverable(digest: digest, privateKey: privateKey)
}
}
Secp256k1Signer, EthSignature 등 서명 함수와 타입의 정확한 정의와 제약은 WalletKit 암호화에서 확인합니다.
서명 대상 — EIP-3009
현재 세션 요청이 지원하는 서명은 EIP-3009 가스리스 전송 계열입니다. eth_signTypedData_v4의 구조화 데이터는 TransferWithAuthorization(가스리스 전송)과 ReceiveWithAuthorization(수신형 결제) 두 가지 primaryType을 다룹니다. 라이브러리가 도메인 분리자와 구조체 해시를 계산해 최종 서명 해시를 만들고, 지갑 앱의 Signer는 그 해시에만 서명합니다.
서명 이후의 가스 대납·온체인 제출은 SCOPE Connect가 제공하는 기능이 아니며, dApp 측 외부 서버가 수행합니다. WalletKit의 역할은 서명 응답을 릴레이로 반환하는 것까지입니다.
서명 요청의 표시 필드(컨트랙트·체인·수취인·금액)는 승인 단계에서 사용자에게 확인시킨 뒤 서명합니다.
소스 트리의 샘플·테스트에 등장하는 개인키 상수는 로컬 연산 검증용입니다. 실제 지갑에서는 이 값을 복사하지 말고 자체 보안 구조와 개인키 생성·보관·백업·복구 정책을 적용하세요.
다음 단계
- 릴레이 전송 — 세션 전송 동작과 오류 처리
- WalletKit 암호화 —
Signer가 사용하는 서명 함수와 타입의 정의