본문으로 건너뛰기

시작하기

월렛이란?​

Wallet은 앱이 상호작용하는 단 하나의 객체입니다. 크리덴셜 저장소를 소유하며, 소수의 서비스를 통해 SDK의 모든 기능을 노출합니다:

wallet.credentials // 목록 / 조회 / 삭제 / DCQL 매칭 / 상태 / 변경 이벤트
wallet.issuance // OpenID4VCI 발급 세션
wallet.presentation // OpenID4VP 원격 + Digital Credentials API
wallet.proximity // ISO 18013-5 대면
wallet.transactions // 감사 이력

월렛은 한 번 조립합니다 — WalletConfig(트러스트 앵커와 발급 기본값)와 WalletPorts(순수한 코어가 필요로 하는 얇은 플랫폼 어댑터 묶음)를 전달한 뒤, 앱 수명 동안 그 인스턴스를 유지합니다. 코어는 Kotlin으로 완전히 구현되어 있고 Swift로 미러링되어 있습니다. 두 구현은 동일한 API 계약을 공유하므로, 아래의 형태는 플랫폼과 무관하게 동일합니다.

월렛 조립하기​

val wallet = Wallet.create(
config = WalletConfig(
trust = TrustConfig(
issuerAnchorsDer = issuerCaDerList, // verifies issuer certs & Token Status Lists
readerAnchorsDer = readerCaDerList, // verifies signed VP requests / mdoc reader auth
),
issuance = IssuanceConfig(
clientId = "wallet-dev",
redirectUri = "eudi-wallet://authorize",
),
),
ports = WalletPorts(
secureAreas = listOf(androidKeystoreSecureArea), // hardware-backed keys
storage = encryptedStorageDriver, // your persistence adapter
http = okHttpTransport, // your HTTP adapter
// walletAttestation, clock, rng, logger, transactionLogStore all have defaults
),
)

WalletConfig​

두 필드 모두 기본값이 있으므로, 트러스트 앵커를 요구하지 않는 이슈어를 상대로 처음 실행할 때는 WalletConfig() 만으로도 유효합니다.

필드타입용도
trustTrustConfigSDK 전반에서 사용하는 X.509 트러스트 앵커입니다. issuerAnchorsDer는 이슈어 서명 인증서와 상태 리스트 서명자를 검증하고, readerAnchorsDer는 서명된 제시 요청(OpenID4VP JAR)과 mdoc 리더 인증을 검증합니다. 앵커는 DER 인코딩된 인증서입니다(List<ByteArray> / [[UInt8]]).
issuanceIssuanceConfig발급 플로우에 적용되는 기본값입니다: clientId(기본값 "wallet-dev")와 redirectUri(기본값 "eudi-wallet://authorize"). 이슈어에 등록한 앱의 값으로 설정하세요.

WalletPorts​

포트는 순수하고 플랫폼 비의존적인 코어와 호스트 사이의 경계입니다. 셋은 필수이고, 나머지는 합리적인 기본값을 가집니다.

포트필수?설명
secureAreas예하나 이상의 SecureArea 키 저장소입니다. 프로덕션에서는 Android에서 Android Keystore, iOS에서 Secure Enclave여야 합니다 — 키는 하드웨어에 보관되며 기기를 절대 벗어나지 않습니다.
storage예크리덴셜과 SDK 상태를 암호화하여 영속화하는 StorageDriver입니다.
http예모든 네트워크 호출에 사용되는 HttpTransport(예: OkHttp / URLSession)입니다. 반드시 request.followRedirects를 준수해야 합니다.
walletAttestation아니오월렛 키 어테스테이션용 WalletAttestationProvider입니다. 기본값은 없음입니다.
clock아니오WalletClock입니다. 기본값은 시스템 클록입니다.
rng아니오Rng입니다. 기본값은 플랫폼 보안 RNG입니다.
logger아니오WalletLogger입니다. 기본값은 없음입니다.
transactionLogStore아니오감사 로그의 백킹 저장소입니다. 기본값은 인메모리 저장소입니다. 재시작 후에도 이력을 유지하려면 영속 저장소를 주입하세요.

:::warning 프로덕션 키 SoftwareSecureArea와 InMemoryStorageDriver는 테스트 전용입니다(테스트 킷에 포함됩니다). 릴리스 빌드에서는 절대 사용하지 마세요 — 키 자료와 크리덴셜을 프로세스 메모리에 보관합니다. :::

단위 테스트에서는 소프트웨어 어댑터를 연결하고 트러스트 앵커를 완전히 생략할 수 있습니다:

// Tests only — never ship SoftwareSecureArea / InMemoryStorageDriver
val wallet = Wallet.create(
config = WalletConfig(), // all defaults
ports = WalletPorts(
secureAreas = listOf(SoftwareSecureArea()),
storage = InMemoryStorageDriver(),
http = fakeHttpTransport,
),
)

직접 어댑터 구현하기​

대부분의 앱은 미리 만들어진 android/ 프리셋(AndroidKeystoreSecureArea, FileStorageDriver, OkHttpTransport, …)에 의존합니다 — Android 어댑터 및 데모를 참고하세요. 다른 플랫폼을 대상으로 하거나 프리셋을 강화하려면 포트를 직접 구현하세요. 어댑터를 올바르게 만드는 두 가지 규칙이 있습니다:

  • **SecureArea**는 개인 키 보관 경계입니다: createKey, publicKey, sign(원시 r||s), keyAgreement, attestation, deleteKey를 구현하고 capabilities를 정직하게 선언하세요(mdoc Mac용으로 구성된 월렛은 capabilities.keyAgreement를 확인합니다). 개인 키는 절대 이 경계를 벗어나면 안 됩니다.
  • **HttpTransport**는 반드시 request.followRedirects를 준수해야 합니다 — OpenID4VCI/VP 플로우는 리다이렉트를 가로채므로, 항상 리다이렉트를 따라가는 트랜스포트는 발급과 제시를 깨뜨립니다.

테스트 킷의 SoftwareSecureArea와 InMemoryStorageDriver를 참조 구현으로 사용하고, 프리셋을 검증하는 것과 동일한 공유 컨트랙트 테스트 스위트로 어댑터를 검증하세요:

// 테스트 킷 — 컨트랙트 위반 시 IllegalStateException을 던집니다.
SecureAreaContract.verify(mySecureArea)
StorageDriverContract.verify(myStorageDriver)

스레드 안전성과 종료​

Wallet은 스레드 안전하며 다중 인스턴스를 지원합니다 — 하나의 인스턴스를 여러 코루틴 / 태스크에서 공유하거나, 같은 프로세스에서 여러 개의 독립적인 월렛을 생성할 수 있습니다. 사용이 끝나면(예: 로그아웃이나 정리 시점) close()를 호출하여 진행 중인 발급, 제시, 근접 세션을 취소하고 리소스를 해제하세요.

wallet.close()

크리덴셜 목록 조회​

wallet.credentials.list()는 저장된 모든 크리덴셜의, 파싱된 포맷 비의존적 뷰를 반환합니다. 조회는 suspend(Kotlin) / async(Swift)입니다. 필터를 지정하지 않으면 전체를 반환합니다(CredentialFilter.All / .all).

val credentials: List<Credential> = wallet.credentials.list() // CredentialFilter.All by default
val one: Credential? = wallet.credentials.get(credentialId)

파싱된 클레임과 라이프사이클 읽기​

각 Credential은 포맷 비의존적 메타데이터(id, format, issuer, display, configurationId)와 lifecycle을 담고 있습니다. 크리덴셜이 발급 완료(issued) 되면, 라이프사이클은 디코딩된 claims(각각 path + value이며, 사람이 읽을 수 있는 형태는 value.display())와 남아 있는 일회용 instances 수를 노출합니다.

val credential = wallet.credentials.get(credentialId) ?: return

// Format-agnostic type name
val typeName = when (val f = credential.format) {
is CredentialFormat.SdJwtVc -> f.vct // SD-JWT VC type
is CredentialFormat.MsoMdoc -> f.docType // mdoc docType
}
credential.issuer?.let { println("Issued by ${it.displayName} (${it.url})") }

// Parsed claims are available once the credential is issued
when (val lifecycle = credential.lifecycle) {
is Lifecycle.Issued -> {
for (claim in lifecycle.claims) {
val path = claim.path.joinToString(".") // e.g. "address.locality"
println("$path = ${claim.value.display()}")
}
println("${lifecycle.instances} unused instance(s) remaining")
}
else -> println("not issued yet")
}

폐기 상태 확인​

status(id)는 크리덴셜의 실시간 폐기 상태를 IETF Token Status List에 대조하여 조회합니다. Unknown은 상태 리스트에 도달할 수 없었음을 의미합니다 — "폐기됨"이 아니라 "재시도"로 취급하세요.

when (wallet.credentials.status(credentialId)) {
CredentialStatus.Valid -> allow()
CredentialStatus.Suspended -> warnTemporarilyBlocked()
CredentialStatus.Invalid -> blockRevoked()
CredentialStatus.Unknown -> retryLater() // status list unreachable
}

DCQL 쿼리로 매칭​

match(dcqlJson)은 저장된 어떤 크리덴셜이 DCQL(dcql_query)을 충족하는지 조회합니다 — 제시 플로우가 사용하는 것과 동일한 엔진이므로, 요청이 도착하기 전에 충족 가능 여부를 미리 확인할 수 있습니다. PID에 대한 다음의 작은 쿼리가 주어졌을 때:

{
"credentials": [
{
"id": "pid",
"format": "dc+sd-jwt",
"meta": { "vct_values": ["urn:eudi:pid:1"] },
"claims": [
{ "path": ["family_name"] },
{ "path": ["given_name"] },
{ "path": ["age_equal_or_over", "18"] }
]
}
]
}

byQuery는 DCQL 크리덴셜 id(위의 "pid")를 키로 사용합니다. 각 MatchedCredential은 어떤 저장된 크리덴셜이 이를 충족하는지, 그리고 정확히 어떤 경로가 공개되는지 알려줍니다.

val match = wallet.credentials.match(dcqlJson)
if (match.satisfiable) {
val candidates = match.byQuery.getValue("pid") // keyed by the DCQL credential id
for (m in candidates) {
println("credential ${m.credential.id} would disclose ${m.disclosedPaths}")
}
}

크리덴셜 변경 관찰​

저장소는 크리덴셜이 추가, 갱신, 또는 삭제될 때마다 변경 이벤트를 발행합니다 — 예를 들어 발급이 완료된 뒤, 제시가 일회용 인스턴스를 소비한 뒤, 또는 상태 갱신 이후입니다. Kotlin은 Flow를, Swift는 AsyncStream을 노출합니다. 이를 사용해 UI를 동기화 상태로 유지하세요.

wallet.credentials.changes.collect { change ->
when (change) {
is CredentialChange.Added -> onAdded(change.id)
is CredentialChange.Updated -> onUpdated(change.id)
is CredentialChange.Removed -> onRemoved(change.id)
}
}

다음 단계​

월렛을 조립했으니, 이제 실제로 활용해 보세요:

  • 발급 — OpenID4VCI로 크리덴셜을 발급받습니다(사전인가 및 인가코드 플로우).
  • 제시 — 원격(OpenID4VP)으로 또는 브라우저(Digital Credentials API)를 통해 제시합니다.
  • 근접 — ISO/IEC 18013-5 device retrieval로 대면 제시합니다.