(KO) PostgreSQL 인증 — pg_hba, SCRAM-SHA-256, SASL
목차
이론적 배경
섹션 제목: “이론적 배경”**인증(authentication)**은 “클라이언트가 자신이 주장하는 신원인가?”라는 질문에 답한다. 서버가 데이터에 대한 접근권을 부여하기 전에 반드시 이 질문에 답해야 한다. 인증은 인가(authorization) — “이 신원은 무엇을 할 수 있는가?” — 와 별개이며, PostgreSQL은 인가를 역할 권한과 행-수준 보안으로 나중에 처리한다. 데이터베이스 인증 서브시스템은 독특하게 노출된 위치에 놓인다. 인증되지 않은 연결에서 실행되고, 공격자가 제어하는 바이트를 처리하며, 버퍼 초과 읽기, 타이밍 누출, 사용자명 오라클 등 어떤 버그도 권한 검사 하나 이루어지기 전에 도달 가능하다. 설계 공간은 네 가지 긴장 관계로 형성된다.
-
저장된 비밀 vs. 전송 중 비밀. 순진한 방식은 평문 비밀번호를 전송하고 저장된 평문 사본과 대조한다. 더 나은 방식은 비밀번호의 단방향 변환인 *검증기(verifier)*를 저장하고 비밀번호 자체는 전송하지 않는다. 가장 강한 방식은 증강 PAKE(augmented PAKE) 계열이다. 서버가 탈취되더라도 공격자는 다른 서버에 재사용할 수 있는 자격증명을 얻지 못하고, 수동 도청자도 재전송 가능한 정보를 얻지 못한다.
-
챌린지-응답 vs. 재전송 가능 토큰. 서버가 신선한 난수 **논스(nonce, 챌린지)**를 보내고 클라이언트가 그것을 응답에 포함시켜야 한다면, 캡처된 응답을 이후 세션에 재전송할 수 없다. MD5 인증은 약한 챌린지-응답이고, SCRAM은 클라이언트+서버 논스 위의 HMAC과 반복 솔팅을 사용하는 강한 챌린지-응답이다.
-
상호 인증(mutual authentication). 단방향 방식은 서버에 클라이언트를 증명한다. 상호 방식은 클라이언트에게도 서버를 증명하므로, 검증기만 탈취한 중간자 공격자를 막는다. SCRAM은 상호 인증 방식이다. 최종 서버 메시지가 클라이언트가 검증하는 서명이기 때문이다.
-
채널 바인딩(channel binding). TLS 위의 상호 인증조차 한 TLS 세션을 종료하고 다른 세션을 여는 공격자가 중계할 수 있다. 채널 바인딩은 인증 교환을 특정 TLS 채널(예: 서버 인증서 해시)에 암호학적으로 묶어, 중계된 교환이 실패하게 만든다. 이것이
SCRAM-SHA-256-PLUS변형이다.
Database System Concepts(Silberschatz et al.)는 인증을 접근 제어 장의 관문으로 다루고, 사전 컴파일된 딕셔너리(“레인보우 테이블”) 공격을 막으려면 비밀번호 저장에 사용자별 솔트가 있는 단방향 해시를 써야 한다고 강조한다. Architecture of a Database System(Hellerstein et al., §“Process Models” 및 §“Admission Control”)은 인증 핸드셰이크가 쿼리 프로세서에 도달하기 전, 연결별 백엔드 설정 단계에 위치한다고 정리한다. PostgreSQL이 ClientAuthentication을 실행하는 위치가 정확히 그곳이다.
현대의 표준 방식은 SCRAM(Salted Challenge Response Authentication Mechanism, RFC 5802)이며, SASL(Simple Authentication and Security Layer, RFC 4422) 프레임워크 위에서 전달된다. SASL은 메타 프로토콜이다. 메커니즘 협상, 불투명한 챌린지/응답 블롭의 시퀀스, 성공/실패 종단을 표준화하고 블롭 내용의 정의는 메커니즘(SCRAM-SHA-256, GSSAPI, OAUTHBEARER 등)에 맡긴다. PostgreSQL은 자체 AUTH_REQ_SASL* 봉투 위에 SCRAM 서버 측을 구현한다. 이 봉투는 v3 와이어 프로토콜의 AuthenticationRequest 'R'와 PasswordMessage 'p' 프레임 위에 다시 계층화된다.
DBMS 공통 설계
섹션 제목: “DBMS 공통 설계”여러 엔진의 인증 서브시스템은 인식 가능한 패턴 집합으로 수렴한다. 이 패턴들을 먼저 이름 붙이면 PostgreSQL의 auth.c가 공유 공간 안에서의 선택으로 읽힌다.
정책 테이블과 메커니즘의 분리
섹션 제목: “정책 테이블과 메커니즘의 분리”거의 모든 서버는 정책 — 어떤 클라이언트에 어떤 메서드가 적용되는가? — 과 메커니즘 — 메서드 X는 실제로 어떻게 실행되는가? — 을 분리한다. 정책은 연결 속성(출발지 주소, 데이터베이스, 요청된 역할, TLS 상태)을 키로 하는 호스트 기반 접근 테이블이다. PostgreSQL의 pg_hba.conf(Host-Based Authentication)가 원형이다. 각 줄은 type database user address method [options] 형태이며, 위에서 아래로 매칭되고 첫 번째 매칭이 적용된다. 매칭 결과는 단일 auth_method 열거값(uaTrust, uaMD5, uaSCRAM, uaGSS 등)이며, 디스패치 switch가 이를 메커니즘 호출로 전환한다.
검증기 저장, 평문 없음
섹션 제목: “검증기 저장, 평문 없음”역할 카탈로그는 스킴 태그가 붙은 검증기를 저장하므로 서버는 확인 방법을 안다. PostgreSQL의 pg_authid.rolpassword는 md5<hex> 문자열이나 SCRAM-SHA-256$<iter>:<salt>$<storedkey>:<serverkey> 문자열을 보유한다. get_password_type이 접두사를 감지한다. 평문 비밀번호는 저장되지 않으며, 평문 타입은 오류 경로에서만 나타난다.
신선한 서버 논스를 포함한 챌린지-응답
섹션 제목: “신선한 서버 논스를 포함한 챌린지-응답”재전송을 막기 위해 서버는 모든 교환에 난수를 기여한다. MD5는 4바이트 솔트를, SCRAM은 클라이언트 자신의 논스에 이어 붙이는 18바이트 base64 인코딩 논스를 보낸다. 응답은 두 논스와 비밀의 함수이므로 일회성이다.
상수 시간 비교와 사용자명 오라클
섹션 제목: “상수 시간 비교와 사용자명 오라클”순진한 구현에서 두 가지 미묘한 누출이 발생한다. 첫째, memcmp로 계산된 해시와 예상 해시를 비교하면 타이밍으로 앞에서 몇 바이트가 일치했는지 누출된다. 안전한 코드는 상수 시간 비교(timingsafe_bcmp)를 사용한다. 둘째, “해당 사용자 없음”을 “잘못된 비밀번호”보다 빠르게(또는 다른 방식으로) 반환하면 로그인 폼이 사용자명 열거 오라클이 된다. 방어책은 **모의 인증(mock authentication)**이다. 사용자가 존재하지 않거나 사용 가능한 비밀이 없을 때, 서버는 그럴듯하지만 실패할 검증기를 만들어 전체 교환을 실행한다. 클라이언트는 타이밍이나 메시지 형태로 “잘못된 사용자”와 “잘못된 비밀번호”를 구별할 수 없다.
vtable을 통한 플러그인 메커니즘
섹션 제목: “vtable을 통한 플러그인 메커니즘”깔끔한 구현은 각 SASL 메커니즘을 작은 인터페이스 — 이름 광고, 상태 초기화, 메시지 하나 처리 — 로 표현하고, 범용 드라이버가 메시지 교환을 반복한다. PostgreSQL의 pg_be_sasl_mech 구조체(get_mechanisms, init, exchange, max_message_length)가 바로 이것이다. CheckSASLAuth가 메커니즘에 무관한 드라이버이고, pg_be_scram_mech와 pg_be_oauth_mech가 두 가지 구현체다.
결정 후·통보 전 훅
섹션 제목: “결정 후·통보 전 훅”감사, 속도 제한, fail2ban 방식의 지연은 클라이언트에게 알리기 전 인증 결과를 관찰하고 싶어한다. 관례적인 위치는 상태가 확정된 후 AuthenticationOk/오류를 보내기 전에 발동하는 단일 훅이다. PostgreSQL의 ClientAuthentication_hook은 정확히 그 시점에 (port, status)와 함께 호출된다.
이론 ↔ PostgreSQL 매핑
섹션 제목: “이론 ↔ PostgreSQL 매핑”| 이론 / 관례 | PostgreSQL 이름 |
|---|---|
| 호스트 기반 정책 테이블 | pg_hba.conf → port->hba->auth_method |
| 정책 조회 | ClientAuthentication 내 hba_getauthmethod(port) |
| 메서드 디스패치 | ClientAuthentication 내 switch (port->hba->auth_method) |
| 저장된 검증기 | pg_authid.rolpassword (shadow_pass) |
| 검증기 스킴 감지 | crypt.c의 get_password_type |
| 와이어 봉투 (요청) | AuthenticationRequest 'R' + AUTH_REQ_* 코드 |
| 와이어 봉투 (응답) | PasswordMessage/SASLInitialResponse/SASLResponse ('p') |
| SASL 드라이버 | auth-sasl.c의 CheckSASLAuth |
| 메커니즘 vtable | pg_be_sasl_mech (pg_be_scram_mech) |
| SCRAM 상태 기계 | scram_exchange (SCRAM_AUTH_INIT/SALT_SENT/FINISHED) |
| 서버 논스 | build_server_first_message (pg_strong_random) |
| 상호 인증 검증기 | build_server_final_message (ServerSignature) |
| 채널 바인딩 | channel_binding_in_use, be_tls_get_certificate_hash |
| 모의 인증 | mock_scram_secret + state->doomed |
| 상수 시간 비교 | verify_client_proof, md5_crypt_verify 내 timingsafe_bcmp |
| 결정 후 훅 | ClientAuthentication_hook |
| 신원 기록 | set_authn_id → MyClientConnectionInfo.authn_id |
PostgreSQL의 접근법
섹션 제목: “PostgreSQL의 접근법”시작 패킷이 파싱된 후(postgres-wire-protocol.md 참조), 백엔드는 ClientAuthentication(port)를 정확히 한 번 호출한다. 이 함수가 이 문서의 핵심이다. 정책을 조회하고, 선택된 메커니즘을 실행하고, 훅을 발동하고, 최종적으로 AuthenticationOk를 보내거나 auth_failed로 종료한다. 나머지 모든 것 — SASL 드라이버, SCRAM 메커니즘, crypt 검증기 — 은 디스패치 switch에서 매달려 있다.
인증 프레임은 와이어 프로토콜의 'R'(AuthenticationRequest)과 'p'(PasswordMessage, SASLInitialResponse, SASLResponse 모두에 공유되는 타입 바이트) 위를 탄다. 'R' 본문의 32비트 AUTH_REQ_* 코드가 클라이언트에게 어떤 하위 프로토콜이 진행 중인지 알린다.
// AUTH_REQ_* — include/libpq/protocol.h#define AUTH_REQ_OK 0 /* User is authenticated */#define AUTH_REQ_PASSWORD 3 /* Password */#define AUTH_REQ_MD5 5 /* md5 password */#define AUTH_REQ_GSS 7 /* GSSAPI without wrap() */#define AUTH_REQ_SASL 10 /* Begin SASL authentication */#define AUTH_REQ_SASL_CONT 11 /* Continue SASL authentication */#define AUTH_REQ_SASL_FIN 12 /* Final SASL message */디스패치: ClientAuthentication
섹션 제목: “디스패치: ClientAuthentication”ClientAuthentication은 먼저 hba_getauthmethod(port)로 정책을 결정(port->hba 채움)한 뒤, clientcert 사전 검사를 수행하고, port->hba->auth_method로 분기한다.
// ClientAuthentication — libpq/auth.c (condensed dispatch)hba_getauthmethod(port);/* ... clientcert pre-checks ... */switch (port->hba->auth_method){ case uaReject: /* explicit "reject" line → FATAL */ case uaImplicitReject: /* no matching hba line → FATAL */ /* ereport(FATAL, ...) with host/user/encryption detail */ case uaMD5: case uaSCRAM: status = CheckPWChallengeAuth(port, &logdetail); break; case uaPassword: status = CheckPasswordAuth(port, &logdetail); break; case uaCert: /* fall through, treated like uaTrust + cert */ case uaTrust: status = STATUS_OK; break; case uaGSS: case uaSSPI: case uaLDAP: case uaPAM: case uaRADIUS: case uaPeer: case uaIdent: case uaOAuth: /* ... delegated to method-specific helpers ... */}종단 로직은 메서드와 무관하게 일관된다. 훅이 발동하고, 그 다음 AUTH_REQ_OK가 전송되거나 프로세스가 종료된다.
// ClientAuthentication — libpq/auth.c (terminal, condensed)if (ClientAuthentication_hook) (*ClientAuthentication_hook) (port, status);
if (status == STATUS_OK) sendAuthRequest(port, AUTH_REQ_OK, NULL, 0);else auth_failed(port, status, logdetail);auth_failed는 클라이언트에게 의도적으로 모호한 메시지(“password authentication failed for user “%s"")를 보내고, 일치한 pg_hba.conf 파일/줄과 logdetail은 서버 로그에만 기록한다. 클라이언트가 탐색에 유용한 정보를 얻지 못하도록 하는 의도적인 비대칭이다.
flowchart TD
A["ClientAuthentication(port)"] --> B["hba_getauthmethod<br/>fills port->hba"]
B --> C{"clientcert<br/>required?"}
C -- "yes, invalid" --> Z["FATAL"]
C -- "ok / n/a" --> D{"auth_method?"}
D -- "uaReject / uaImplicitReject" --> Z
D -- "uaTrust / uaCert" --> OK["status = STATUS_OK"]
D -- "uaPassword" --> P["CheckPasswordAuth<br/>(plaintext)"]
D -- "uaMD5 / uaSCRAM" --> PW["CheckPWChallengeAuth"]
D -- "uaGSS/SSPI/LDAP/...<br/>(adjacent — see cross-refs)" --> EXT["method helper"]
P --> H
PW --> H
OK --> H
EXT --> H["ClientAuthentication_hook(port, status)"]
H --> R{"status == OK?"}
R -- "yes" --> S["sendAuthRequest AUTH_REQ_OK"]
R -- "no" --> F["auth_failed → FATAL"]
그림 1 — ClientAuthentication 제어 흐름. 정책 조회(hba_getauthmethod)가 메서드 디스패치에 앞선다. 모든 경로가 ClientAuthentication_hook으로 수렴한 뒤 AUTH_REQ_OK 또는 auth_failed로 이어진다. GSS/SSPI/LDAP/PAM/RADIUS/ident/peer는 switch의 실제 분기이지만 이 문서의 범위 밖이다 — 교차 참조를 보라.
MD5 vs. SCRAM 선택: CheckPWChallengeAuth
섹션 제목: “MD5 vs. SCRAM 선택: CheckPWChallengeAuth”uaMD5와 uaSCRAM은 모두 CheckPWChallengeAuth를 거치며, 이 함수는 hba 줄만이 아니라 저장된 비밀의 타입으로 실제 메커니즘을 결정한다. 규칙은 이렇다. md5 hba 줄은 저장된 비밀이 진짜 MD5 해시일 때만 MD5를 사용하고, 그렇지 않으면 SCRAM으로 승급한다. scram 줄은 항상 SCRAM을 사용하며, MD5 비밀만 가진 사용자는 실패한다.
// CheckPWChallengeAuth — libpq/auth.c (condensed)shadow_pass = get_role_password(port->user_name, logdetail);
if (!shadow_pass) pwtype = Password_encryption; /* user missing: blend in */else pwtype = get_password_type(shadow_pass);
if (port->hba->auth_method == uaMD5 && pwtype == PASSWORD_TYPE_MD5) auth_result = CheckMD5Auth(port, shadow_pass, logdetail);else auth_result = CheckSASLAuth(&pg_be_scram_mech, port, shadow_pass, logdetail);
if (auth_result == STATUS_OK) set_authn_id(port, port->user_name);get_role_password가 NULL을 반환하면(사용자 없음, 비밀번호 없음, 만료된 비밀번호) shadow_pass는 NULL로 메커니즘에 전달되고, 메커니즘은 실패 예정 교환을 실행한다. 호출 후 Assert(auth_result != STATUS_OK)는 NULL 비밀이 절대 인증을 통과할 수 없다는 불변 조건을 코드로 표현한다.
MD5: 단일 솔팅 라운드
섹션 제목: “MD5: 단일 솔팅 라운드”CheckMD5Auth는 4바이트 랜덤 솔트를 생성하고, AUTH_REQ_MD5와 함께 전송하며, md5_crypt_verify로 응답을 검증한다.
// CheckMD5Auth — libpq/auth.c (condensed)if (!pg_strong_random(md5Salt, 4)) { /* LOG; STATUS_ERROR */ }sendAuthRequest(port, AUTH_REQ_MD5, md5Salt, 4);passwd = recv_password_packet(port);if (passwd == NULL) return STATUS_EOF;if (shadow_pass) result = md5_crypt_verify(port->user_name, shadow_pass, passwd, md5Salt, 4, logdetail);else result = STATUS_ERROR;검증기는 저장된 해시(md5 접두사 제외)를 챌린지 솔트로 MD5 해싱해 예상 응답을 재계산하고, 상수 시간으로 비교한다. MD5는 PG18에서 deprecated다. encrypt_password는 MD5 비밀이 설정될 때마다 WARNING을 발행하지만, 아직 지원은 된다.
SASL 위의 SCRAM-SHA-256
섹션 제목: “SASL 위의 SCRAM-SHA-256”uaSCRAM(과 SCRAM으로 승급된 uaMD5 경로)은 CheckSASLAuth(&pg_be_scram_mech, …)를 호출한다. SASL 봉투와 SCRAM 메커니즘이 만나는 지점이다. 드라이버는 auth-sasl.c에, 메커니즘은 auth-scram.c에 있다. 메커니즘 목록 광고 이후 프로토콜 메시지는 세 번이다.
flowchart TD
A["CheckSASLAuth: get_mechanisms<br/>advertise list"] --> B["send AUTH_REQ_SASL<br/>(SCRAM-SHA-256-PLUS, SCRAM-SHA-256)"]
B --> C["recv SASLInitialResponse 'p'<br/>selected_mech + client-first"]
C --> D["mech->init<br/>scram_init: load secret or mock+doom"]
D --> E["mech->exchange #1<br/>scram_exchange SCRAM_AUTH_INIT"]
E --> F["read_client_first_message<br/>build_server_first_message"]
F --> G["send AUTH_REQ_SASL_CONT<br/>r=nonce,s=salt,i=iters → CONTINUE"]
G --> H["recv SASLResponse 'p'<br/>client-final + proof"]
H --> I["mech->exchange #2<br/>scram_exchange SCRAM_AUTH_SALT_SENT"]
I --> J["read_client_final_message<br/>verify_final_nonce"]
J --> K{"verify_client_proof<br/>and not doomed?"}
K -- "no" --> L["PG_SASL_EXCHANGE_FAILURE<br/>→ STATUS_ERROR → auth_failed"]
K -- "yes" --> M["build_server_final_message<br/>v=ServerSignature"]
M --> N["send AUTH_REQ_SASL_FIN<br/>→ SUCCESS → STATUS_OK"]
N --> O["ClientAuthentication: AUTH_REQ_OK"]
그림 2 — SCRAM-SHA-256 SASL 교환을 드라이버/메커니즘 호출 흐름으로 표현. 드라이버(CheckSASLAuth)는 AUTH_REQ_SASL* 봉투와 메시지 루프를 담당하고, 메커니즘(scram_exchange)은 SCRAM 암호화를 담당한다. 첫 번째 클라이언트 메시지는 선택된 메커니즘 이름을 담은 SASLInitialResponse이고, 이후는 단순한 SASLResponse 페이로드다. 성공 시 드라이버는 서버 서명을 담은 AUTH_REQ_SASL_FIN을 보내고, 클라이언트가 이를 검증해 서버를 인증한다.
암호화의 핵심은 저장된 키(StoredKey) / 서버 키(ServerKey) 분리다. pg_authid 비밀은 SCRAM-SHA-256$<iter>:<salt>$<StoredKey>:<ServerKey> 형태다. SHA-256 기반 파생 체인은 다음과 같다.
SaltedPassword = PBKDF2(SASLprep(password), salt, iterations)— 오프라인 무차별 대입을 비싸게 만드는 반복 솔팅.ClientKey = HMAC(SaltedPassword, "Client Key"),ServerKey = HMAC(SaltedPassword, "Server Key").StoredKey = H(ClientKey)— 서버가ClientKey없이 증명을 검증할 수 있게 하는 추가 해시.
클라이언트는 ClientProof = ClientKey XOR ClientSignature를 보내 비밀번호 지식을 증명한다. 여기서 ClientSignature = HMAC(StoredKey, AuthMessage)이고, AuthMessage는 client-first-bare, server-first, client-final-without-proof의 연결이다. StoredKey만 보유한 서버는 같은 ClientSignature를 계산하고, 증명에서 XOR로 복원한 후보 ClientKey를 해싱해 저장된 StoredKey와 상수 시간으로 비교한다.
flowchart LR
P["password"] --> SP["SaltedPassword<br/>= PBKDF2(pw, salt, iter)"]
SP --> CK["ClientKey<br/>= HMAC(SP, 'Client Key')"]
SP --> SK["ServerKey<br/>= HMAC(SP, 'Server Key')"]
CK --> ST["StoredKey<br/>= H(ClientKey)"]
ST --> STORE[("pg_authid:<br/>iter,salt,StoredKey,ServerKey")]
SK --> STORE
PROOF["client ClientProof"] --> XOR["XOR with<br/>HMAC(StoredKey, AuthMessage)"]
ST --> XOR
XOR --> CAND["candidate ClientKey"]
CAND --> HH["H(candidate)"]
HH --> CMP{"timingsafe_bcmp<br/>== StoredKey?"}
STORE --> CMP
CMP -- "yes" --> OKK["authenticated"]
CMP -- "no" --> FAIL["fail"]
그림 3 — SCRAM-SHA-256 키 파생과 증명 검증. 왼쪽 체인은 pg_be_scram_build_secret이 CREATE/ALTER ... PASSWORD 시점에 한 번 계산해 pg_authid에 저장하는 것이다. 오른쪽 경로는 verify_client_proof가 로그인마다 수행하는 것이다. StoredKey/ServerKey만 저장되므로 pg_authid 덤프는 로그인에 재사용할 수 없다. SHA-256을 역산하지 않고서는 ClientKey를 복원할 수 없기 때문이다. 이것이 §이론적 배경에서 설명한 증강 속성이다.
소스 탐방
섹션 제목: “소스 탐방”심볼 이름을 기준으로 탐색한다. 줄 번호는 아래 위치 힌트 표에만 있으며, 커밋
273fe94기준이다.git grep -n '<symbol>' src/backend/libpq/로 재위치 확인이 가능하다.
SASL 드라이버 (libpq/auth-sasl.c)
섹션 제목: “SASL 드라이버 (libpq/auth-sasl.c)”CheckSASLAuth는 메커니즘에 무관하다. 메커니즘 목록을 광고하고, 'p' 메시지를 읽어 mech->exchange에 페이로드를 전달하는 루프를 메커니즘이 종단 상태를 반환할 때까지 반복한다. 첫 번째 메시지는 특별하다. 선택된 메커니즘 이름과 선택적 초기 클라이언트 응답을 담은 SASLInitialResponse다.
// CheckSASLAuth — libpq/auth-sasl.c (condensed)mech->get_mechanisms(port, &sasl_mechs);appendStringInfoChar(&sasl_mechs, '\0'); /* terminate list */sendAuthRequest(port, AUTH_REQ_SASL, sasl_mechs.data, sasl_mechs.len);
initial = true;do { pq_startmsgread(); mtype = pq_getbyte(); if (mtype != PqMsg_SASLResponse) { /* EOF or PROTOCOL_VIOLATION */ }
pq_getmessage(&buf, mech->max_message_length);
if (initial) { selected_mech = pq_getmsgrawstring(&buf); opaq = mech->init(port, selected_mech, shadow_pass); /* may doom */ inputlen = pq_getmsgint(&buf, 4); input = (inputlen == -1) ? NULL : pq_getmsgbytes(&buf, inputlen); initial = false; } else { inputlen = buf.len; input = pq_getmsgbytes(&buf, buf.len); }
result = mech->exchange(opaq, input, inputlen, &output, &outputlen, logdetail);
if (output) { if (result == PG_SASL_EXCHANGE_SUCCESS) sendAuthRequest(port, AUTH_REQ_SASL_FIN, output, outputlen); else sendAuthRequest(port, AUTH_REQ_SASL_CONT, output, outputlen); }} while (result == PG_SASL_EXCHANGE_CONTINUE);
return (result == PG_SASL_EXCHANGE_SUCCESS) ? STATUS_OK : STATUS_ERROR;불변 조건 가드에 주목하라. PG_SASL_EXCHANGE_FAILURE임에도 output이 존재하는 경우는 SASL 위반이며 elog(ERROR, "output message found after SASL exchange failure")를 발생시킨다. 드라이버는 SCRAM 페이로드 내부를 절대 들여다보지 않는다. 이 불투명성이 SASL 레이어의 존재 이유다.
핵심 심볼:
CheckSASLAuth— 드라이버 루프. 광고 → init → exchange* → 종단.pg_be_sasl_mech— 메커니즘 vtable 타입 (get_mechanisms,init,exchange,max_message_length).PqMsg_SASLResponse— 루프 내 모든 클라이언트 메시지에서 기대하는'p'타입 바이트.
SCRAM 메커니즘 (libpq/auth-scram.c)
섹션 제목: “SCRAM 메커니즘 (libpq/auth-scram.c)”메커니즘은 pg_be_sasl_mech로 등록된다.
// pg_be_scram_mech — libpq/auth-scram.cconst pg_be_sasl_mech pg_be_scram_mech = { scram_get_mechanisms, scram_init, scram_exchange, PG_MAX_SASL_MESSAGE_LENGTH};**scram_get_mechanisms**는 SCRAM-SHA-256-PLUS를 먼저 광고한다(port->ssl_in_use일 때만, 채널 바인딩에 TLS가 필요하기 때문). 그 다음 SCRAM-SHA-256을 NUL 구분자로 이어서 광고한다.
// scram_get_mechanisms — libpq/auth-scram.c (condensed)#ifdef USE_SSL if (port->ssl_in_use) { appendStringInfoString(buf, SCRAM_SHA_256_PLUS_NAME); appendStringInfoChar(buf, '\0'); }#endif appendStringInfoString(buf, SCRAM_SHA_256_NAME); appendStringInfoChar(buf, '\0');**scram_init**은 클라이언트가 선택한 메커니즘을 파싱(channel_binding_in_use 설정)하고, parse_scram_secret으로 저장된 비밀을 로드한다. 비밀이 없거나 SCRAM 형식이 아니면 mock_scram_secret으로 대체하고 state->doomed = true를 설정한다. 교환은 완료까지 실행되고 실패한다. 잘못된 비밀번호와 구별할 수 없다.
// scram_init — libpq/auth-scram.c (condensed, mock fallback)if (!got_secret) { mock_scram_secret(state->port->user_name, &state->hash_type, &state->iterations, &state->key_length, &state->salt, state->StoredKey, state->ServerKey); state->doomed = true;}**scram_exchange**는 2단계 상태 기계다(SCRAM_AUTH_INIT → SCRAM_AUTH_SALT_SENT → SCRAM_AUTH_FINISHED). doomed 확인은 verify_client_proof 실행 이후에 의도적으로 적용된다. 실패 예정 교환에서도 타이밍을 균일하게 유지하기 위해 증명 계산은 수행된다.
// scram_exchange — libpq/auth-scram.c (condensed)switch (state->state) { case SCRAM_AUTH_INIT: read_client_first_message(state, input); *output = build_server_first_message(state); state->state = SCRAM_AUTH_SALT_SENT; result = PG_SASL_EXCHANGE_CONTINUE; break; case SCRAM_AUTH_SALT_SENT: read_client_final_message(state, input); if (!verify_final_nonce(state)) { /* PROTOCOL_VIOLATION */ } /* NB: proof computed even when doomed, to thwart timing attacks */ if (!verify_client_proof(state) || state->doomed) { result = PG_SASL_EXCHANGE_FAILURE; break; } *output = build_server_final_message(state); result = PG_SASL_EXCHANGE_SUCCESS; state->state = SCRAM_AUTH_FINISHED; break;}**build_server_first_message**는 pg_strong_random으로 서버 논스를 생성한다(18 raw 바이트, base64 인코딩). r=<clientnonce><servernonce>,s=<salt>,i=<iterations>를 출력한다.
// build_server_first_message — libpq/auth-scram.c (condensed)if (!pg_strong_random(raw_nonce, SCRAM_RAW_NONCE_LEN)) { /* ERROR */ }encoded_len = pg_b64_encode(raw_nonce, SCRAM_RAW_NONCE_LEN, state->server_nonce, encoded_len);state->server_first_message = psprintf("r=%s%s,s=%s,i=%d", state->client_nonce, state->server_nonce, state->salt, state->iterations);**verify_client_proof**는 핵심 검증이다. auth-message 튜플을 StoredKey로 HMAC 해서 ClientSignature를 구하고, 클라이언트의 ClientProof와 XOR해 후보 ClientKey를 복원한다. 그것을 한 번 해싱(scram_H)하고 저장된 StoredKey와 timingsafe_bcmp로 비교한다.
// verify_client_proof — libpq/auth-scram.c (condensed)pg_hmac_init(ctx, state->StoredKey, state->key_length);pg_hmac_update(ctx, client_first_message_bare, ...);pg_hmac_update(ctx, (uint8 *) ",", 1);pg_hmac_update(ctx, server_first_message, ...);pg_hmac_update(ctx, (uint8 *) ",", 1);pg_hmac_update(ctx, client_final_message_without_proof, ...);pg_hmac_final(ctx, ClientSignature, state->key_length);
for (i = 0; i < state->key_length; i++) state->ClientKey[i] = state->ClientProof[i] ^ ClientSignature[i];
scram_H(state->ClientKey, state->hash_type, state->key_length, client_StoredKey, &errstr);
if (timingsafe_bcmp(client_StoredKey, state->StoredKey, state->key_length) != 0) return false;return true;**build_server_final_message**는 ServerSignature = HMAC(ServerKey, auth-message)를 계산하고 v=<base64>를 반환한다. 클라이언트는 자신의 ServerKey로 같은 HMAC를 재계산하고 일치 여부를 확인한다. 이것이 서버가 검증기를 보유하고 있음을 증명하는 상호 인증 단계다.
채널 바인딩은 read_client_final_message 내부에서 강제된다. channel_binding_in_use일 때 서버는 be_tls_get_certificate_hash로 예상 c= 값을 재계산하고 클라이언트가 보낸 값과 strcmp한다. 채널 바인딩을 사용하지 않을 때는 c= 값이 정확히 biws("n,," base64) 또는 eSws("y,," base64)여야 하고 원래의 cbind_flag와 일치해야 한다.
// read_client_final_message — libpq/auth-scram.c (condensed, cbind)channel_binding = read_attr_value(&p, 'c');if (state->channel_binding_in_use) { cbind_data = be_tls_get_certificate_hash(state->port, &cbind_data_len); /* cbind_input = "p=tls-server-end-point,," || cbind_data, then base64 */ if (strcmp(channel_binding, b64_message) != 0) ereport(ERROR, (errmsg("SCRAM channel binding check failed")));} else { if (!(strcmp(channel_binding, "biws") == 0 && state->cbind_flag == 'n') && !(strcmp(channel_binding, "eSws") == 0 && state->cbind_flag == 'y')) ereport(ERROR, (errmsg("unexpected SCRAM channel-binding attribute ...")));}read_client_first_message의 gs2 플래그 파싱은 다운그레이드 공격을 차단한다. SCRAM-SHA-256-PLUS를 선택한 클라이언트는 반드시 p=를 보내야 한다. 서버가 바인딩을 지원하는데 y(서버가 지원하지 않는다고 생각하지만 클라이언트는 지원)를 보내는 클라이언트는 거부된다.
핵심 심볼: scram_get_mechanisms, scram_init, scram_exchange, read_client_first_message, build_server_first_message, read_client_final_message, verify_final_nonce, verify_client_proof, build_server_final_message, parse_scram_secret, mock_scram_secret, scram_mock_salt, pg_be_scram_build_secret, scram_verify_plain_password.
비밀번호 검증기 (libpq/crypt.c)
섹션 제목: “비밀번호 검증기 (libpq/crypt.c)”crypt.c는 pg_authid.rolpassword 위의 스킴 인식 레이어다.
get_role_password—SearchSysCache1(AUTHNAME, …),rolpassword를 추출하고rolvaliduntil을 강제한다. 사용자 없음, 비밀번호 없음, 만료된 비밀번호에는 NULL을 반환한다(로그 전용logdetail포함). 이 NULL이 상위에서 모의 인증을 활성화한다.get_password_type— 스킴을 감지한다.md5접두사 +MD5_PASSWD_LEN+ 16진수 →PASSWORD_TYPE_MD5;parse_scram_secret성공 →PASSWORD_TYPE_SCRAM_SHA_256; 그 외 →PASSWORD_TYPE_PLAINTEXT.md5_crypt_verify— 저장된 해시를 챌린지 솔트로 재해싱하고 클라이언트 응답과timingsafe_bcmp로 비교한다.plain_crypt_verify—uaPassword가 사용한다. 저장된 비밀이 SCRAM이면scram_verify_plain_password(평문에서 ServerKey 재계산)를, MD5면 해싱-비교를 호출한다. 모두 상수 시간이다.encrypt_password—CREATE/ALTER ... PASSWORD경로.pg_be_scram_build_secret으로 SCRAM 비밀을 구축하거나 MD5에 경고를 발행한다.
// md5_crypt_verify — libpq/crypt.c (condensed)pg_md5_encrypt(shadow_pass + strlen("md5"), md5_salt, md5_salt_len, crypt_pwd, &errstr);if (strlen(client_pass) == strlen(crypt_pwd) && timingsafe_bcmp(client_pass, crypt_pwd, strlen(crypt_pwd)) == 0) retval = STATUS_OK;else retval = STATUS_ERROR; /* logdetail: "Password does not match" */// plain_crypt_verify — libpq/crypt.c (condensed, SCRAM branch)switch (get_password_type(shadow_pass)) { case PASSWORD_TYPE_SCRAM_SHA_256: if (scram_verify_plain_password(role, client_pass, shadow_pass)) return STATUS_OK; /* else logdetail + STATUS_ERROR */ case PASSWORD_TYPE_MD5: pg_md5_encrypt(client_pass, (uint8 *) role, strlen(role), crypt_client_pass, &errstr); /* timingsafe_bcmp vs shadow_pass */}auth.c의 접착 코드
섹션 제목: “auth.c의 접착 코드”ClientAuthentication— 정책 조회 + 디스패치 + 훅 + 종단.auth_failed— 클라이언트에게 모호한 메시지, 서버 로그에 상세 내용, 비밀번호 계열에ERRCODE_INVALID_PASSWORD.sendAuthRequest—pq_beginmessage(PqMsg_AuthenticationRequest)+pq_sendint32(areq)+ 선택적 본문.AUTH_REQ_OK와AUTH_REQ_SASL_FIN을 제외한 모든 코드에서 플러시한다.recv_password_packet—'p'PasswordMessage를 읽고,PG_MAX_AUTH_TOKEN_LENGTH로 크기를 제한하며, 빈 비밀번호를 거부한다.CheckPasswordAuth/CheckPWChallengeAuth/CheckMD5Auth— 세 가지 비밀번호 드라이버.set_authn_id— 인증된 신원을MyClientConnectionInfo.authn_id에 정확히 한 번 기록한다. 두 번째 호출은FATAL이다(두 제공자가 서로 인증했다고 주장하는 경우를 방어).ClientAuthentication_hook— 확장점.
위치 힌트 (2026-06-05, REL_18 273fe94 기준)
섹션 제목: “위치 힌트 (2026-06-05, REL_18 273fe94 기준)”| 심볼 | 파일 | 줄 |
|---|---|---|
ClientAuthentication_hook (global) | libpq/auth.c | 223 |
auth_failed | libpq/auth.c | 239 |
set_authn_id | libpq/auth.c | 341 |
ClientAuthentication | libpq/auth.c | 379 |
sendAuthRequest | libpq/auth.c | 677 |
recv_password_packet | libpq/auth.c | 707 |
CheckPasswordAuth | libpq/auth.c | 788 |
CheckPWChallengeAuth | libpq/auth.c | 823 |
CheckMD5Auth | libpq/auth.c | 883 |
CheckSASLAuth | libpq/auth-sasl.c | 44 |
pg_be_scram_mech | libpq/auth-scram.c | 114 |
scram_get_mechanisms | libpq/auth-scram.c | 206 |
scram_init | libpq/auth-scram.c | 240 |
scram_exchange | libpq/auth-scram.c | 352 |
pg_be_scram_build_secret | libpq/auth-scram.c | 483 |
scram_verify_plain_password | libpq/auth-scram.c | 523 |
parse_scram_secret | libpq/auth-scram.c | 600 |
mock_scram_secret | libpq/auth-scram.c | 697 |
read_client_first_message | libpq/auth-scram.c | 913 |
verify_client_proof | libpq/auth-scram.c | 1149 |
build_server_first_message | libpq/auth-scram.c | 1202 |
read_client_final_message | libpq/auth-scram.c | 1266 |
build_server_final_message | libpq/auth-scram.c | 1412 |
scram_mock_salt | libpq/auth-scram.c | 1471 |
get_role_password | libpq/crypt.c | 38 |
get_password_type | libpq/crypt.c | 90 |
encrypt_password | libpq/crypt.c | 117 |
md5_crypt_verify | libpq/crypt.c | 202 |
plain_crypt_verify | libpq/crypt.c | 257 |
AUTH_REQ_* 상수 | include/libpq/protocol.h | 74 |
소스 검증 (2026-06-05 기준)
섹션 제목: “소스 검증 (2026-06-05 기준)”커밋
273fe94의 소스에 대한 사실. 외부 자료 없이 소스만으로 확인 가능한 내용이다. 미결 질문은 이어진다.
검증된 사실
섹션 제목: “검증된 사실”-
ClientAuthentication이 유일한 디스패치 지점이며, 모든 경로가 종단 메시지 전ClientAuthentication_hook을 통과한다.auth.c의ClientAuthentication에서 확인. 훅은 메서드 switch와CheckCertAuth사후 검사 이후,sendAuthRequest(…, AUTH_REQ_OK, …)또는auth_failed이전에(port, status)와 함께 호출된다. 감사/지연 훅이 클라이언트 왕복 없이 상태가STATUS_OK로 설정되는uaTrust를 포함한 모든 메서드의 실제 결과를 볼 수 있음을 보장한다. -
uaMD5는 투명하게 SCRAM을 실행할 수 있다.CheckPWChallengeAuth에서 확인. MD5는port->hba->auth_method == uaMD5 && pwtype == PASSWORD_TYPE_MD5일 때만 사용된다. 그 외에는CheckSASLAuth(&pg_be_scram_mech, …)가 실행된다. SCRAM 비밀을 가진 역할에md5hba 줄이 있으면 SCRAM이 수행되고, MD5 비밀만 있는 역할에scram줄이 있으면 SCRAM이 SCRAM 불가능 비밀을 상대로 실행되어 실패한다. -
모의 인증은 타이밍과 메시지 형태 모두에서 대칭적이다.
CheckPWChallengeAuth,scram_init,scram_exchange에서 확인.shadow_pass가 NULL이면scram_init이 결정론적 모의 비밀(mock_scram_secret→scram_mock_salt, 사용자명과 클러스터 모의 논스로 시드됨)을 만들고doomed = true를 설정한다. 전체 server-first / client-final 교환이 여전히 실행되고,verify_client_proof는doomed단락 이전에 호출된다. 암호화 작업량은 존재하는 사용자와 존재하지 않는 사용자 모두 동일하다. -
서버는
ClientKey를 저장하거나 전송하지 않는다.parse_scram_secret(비밀 형식SCRAM-SHA-256$<iter>:<salt>$<StoredKey>:<ServerKey>)과verify_client_proof(서버가 클라이언트 증명에서 후보ClientKey를 복원하고 재해싱해StoredKey와 비교)에서 확인.pg_authid덤프는StoredKey/ServerKey만 얻는다. 직접 로그인 자격증명으로 쓸 수 없다. -
모든 자격증명 비교는 상수 시간이다.
verify_client_proof,verify_final_nonce,scram_verify_plain_password,md5_crypt_verify,plain_crypt_verify모두에서 확인. 비밀 의존 비교에memcmp/strcmp대신timingsafe_bcmp를 사용한다. -
set_authn_id는 단일 할당을 강제한다.set_authn_id에서 확인.MyClientConnectionInfo.authn_id가 이미 설정된 상태에서 두 번째 호출이 오면FATAL(“authentication identifier set more than once”)을 발생시킨다. 두 제공자가 각자 연결을 인증했다고 믿는 상황을 방어한다. -
AUTH_REQ_OK와AUTH_REQ_SASL_FIN은sendAuthRequest에서 플러시되지 않는다.sendAuthRequest에서 확인.pq_flush()는if (areq != AUTH_REQ_OK && areq != AUTH_REQ_SASL_FIN)으로 가드된다. 이 두 메시지는 이후의ParameterStatus/BackendKeyData/ReadyForQuery묶음과 함께 전송되어 시스템 호출을 절약한다. -
채널 바인딩은 TLS 아래서만 광고되고 재계산으로 강제된다.
scram_get_mechanisms(SCRAM-SHA-256-PLUS는#ifdef USE_SSL과port->ssl_in_use조건 하에서만 추가됨)와read_client_final_message(서버가be_tls_get_certificate_hash로c=값을 재계산하고strcmp)에서 확인.read_client_first_message의 gs2 플래그 처리는 서버가 바인딩을 지원할 때y플래그 다운그레이드를 거부한다. -
빈 비밀번호는 와이어 단계에서 거부된다.
recv_password_packet에서 확인.buf.len == 1(NUL 종단자만)이면ERRCODE_INVALID_PASSWORD(“empty password returned by client”)를 발생시킨다. 카탈로그 수준 빈 비밀번호 확인이 적용되지 않는 외부 시스템(PAM/LDAP/RADIUS)을 포함해 모든 경우를 커버한다.
미결 질문
섹션 제목: “미결 질문”-
clientcert=verify-full과 SCRAM 채널 바인딩의 상호작용.ClientAuthentication은 비밀번호/SASL 교환 성공 후clientcert == clientCertFull일 때CheckCertAuth를 실행한다. 이것이SCRAM-SHA-256-PLUS채널 바인딩(둘 다 서버 인증서를 건드린다)과 어떻게 구성되는지는 여기서 추적하지 않는다. 경로:auth.c의CheckCertAuth와be-secure-openssl.c의be_tls_get_certificate_hash. -
scram_sha_256_iterationsGUC와 저장된 반복 횟수. 새 비밀은scram_sha_256_iterations(기본값SCRAM_SHA_256_DEFAULT_ITERATIONS)를 사용하지만, 검증은 비밀에 저장된 반복 횟수를 사용한다. 관리자가 GUC를 낮추거나 올릴 때의 마이그레이션(기존 비밀은 재설정 전까지 기존 횟수를 유지)은 언급하되 여기서 다루지는 않는다. -
OAUTHBEARER 메커니즘 (
pg_be_oauth_mech). PG18은 같은CheckSASLAuth드라이버를 재사용하는 OAuth SASL 메커니즘을 추가했다(uaOAuth → CheckSASLAuth(&pg_be_oauth_mech, …)). 검증기-모듈 인터페이스와 토큰 흐름은 이 문서의 범위 밖이다. 경로:libpq/auth-oauth.c.
PostgreSQL 너머 — 비교 설계와 연구 프런티어
섹션 제목: “PostgreSQL 너머 — 비교 설계와 연구 프런티어”분석이 아닌 포인터. 각 항목은 후속 문서를 위한 출발 손잡이다.
-
SCRAM vs. 진정한 증강 PAKE (OPAQUE). SCRAM은 약한 의미에서만 “증강”이다.
pg_authid덤프로 직접 사용 가능한 자격증명을 얻을 수 없지만, 성공한 교환을 수동 관찰한 공격자가 나중에StoredKey를 탈취하면 오프라인 딕셔너리 공격이 가능하다. 또한 서버는 검증기를 공유하는 제3 서버를 향해 클라이언트를 가장하기에 충분한 정보를 갖게 된다. 현대 비대칭 PAKE(OPAQUE, RFC 초안; CPace)는 이 간격을 닫는다. 서버는 일시적으로도 비밀번호에 상당하는 것을 보지 않는다. OPAQUE의 사전 계산 저항성과 SCRAM의 반복 솔팅 모델 비교가 PG18이 무엇을 보호하고 무엇을 보호하지 않는지를 명확히 할 것이다. -
MD5 폐기 타임라인. PG18은 MD5 비밀이 설정될 때마다
encrypt_password에서WARNING을 발행(md5_password_warnings)해 제거 신호를 보낸다. 역할명으로 솔팅하는 비솔팅 약점(같은 비밀번호라도 역할이 다르면 다른 해시가 나오지만, 챌린지 솔트는 4바이트에 불과해 무차별 대입이 가능하다)은 보안 문제다. 카탈로그 업그레이드 경로(ALTER ROLE ... PASSWORD재해싱,password_encryptionGUC)는postgres-roles-acl.md동반 문서에 적합하다. -
확장 이음새로서의 SASL 프레임워크.
CheckSASLAuth는 완전히 메커니즘에 무관하다.pg_be_scram_mech와pg_be_oauth_mech는pg_be_sasl_mech의 두 인스턴스다. 서드파티 확장이 새 메커니즘을 등록할 수는 있지만, PostgreSQL은 공개 등록 API를 제공하지 않는다(ClientAuthentication의 디스패치는 하드코딩된 switch다).ClientAuthentication_hook과 대조하면 좋다. 이 훅은 공개 이음새이지만 결과만 관찰한다. “플러그인 인증 메커니즘” API에 무엇이 필요한지에 대한 연구(Linux PAM, Cyrus 같은 SASL 라이브러리와 비교)는 자연스러운 설계 노트가 된다. -
실제의
ClientAuthentication_hook. 실제 확장들이 이 훅을 사용하는 용도는 다양하다. 실패 로그인 속도 제한/지수 백오프(트리 내 예시인 contrib의auth_delay는 참조 목적으로만 언급), 보안 이벤트 감사(pgaudit), IP 허용/거부 오버레이 등이다. 훅의(port, status)서명과 발동 시점(결정 후, 통보 전)이 이를 표준 위치로 만든다.Port에서 각 훅이 읽는 내용의 카탈로그는postgres-hooks.md를 완성할 것이다. -
연결 풀러/프록시 인증. PgBouncer, Odyssey, 향후 도입될 코어 연결 풀링은 SCRAM을 투명하게 통과시키거나 종료하고 백엔드에 재인증해야 한다. 채널 바인딩과는 잘 맞지 않는다(풀러의 TLS 인증서가 백엔드 인증서와 다르다).
auth_query/auth_user풀러 패턴이pg_authid에서StoredKey/ServerKey를 읽는 방식은 검증기 형식의 실제 스트레스 테스트다.
프로토콜 및 암호화 명세
섹션 제목: “프로토콜 및 암호화 명세”- RFC 5802 — Salted Challenge Response Authentication Mechanism (SCRAM) SASL and GSS-API Mechanisms.
read_client_first_message/read_client_final_message/build_server_first_message에 인용된 메시지 문법이 이 RFC에서 나온다. - RFC 4422 — Simple Authentication and Security Layer (SASL). 봉투(
CheckSASLAuth)가 이 프레임워크를 따른다. - RFC 5929 — Channel Bindings for TLS (
tls-server-end-point). - PostgreSQL 문서, “Client Authentication” 장 —
pg_hba.conf문법,password_encryption, SCRAM/채널 바인딩 설정.
PostgreSQL 소스 (/data/hgryoo/references/postgres, REL_18 273fe94)
섹션 제목: “PostgreSQL 소스 (/data/hgryoo/references/postgres, REL_18 273fe94)”src/backend/libpq/auth.c—ClientAuthentication,auth_failed,set_authn_id,sendAuthRequest,recv_password_packet,CheckPasswordAuth,CheckPWChallengeAuth,CheckMD5Auth,ClientAuthentication_hook.src/backend/libpq/auth-sasl.c—CheckSASLAuth(SASL 드라이버 루프).src/backend/libpq/auth-scram.c—pg_be_scram_mech,scram_get_mechanisms,scram_init,scram_exchange,read_client_first_message,build_server_first_message,read_client_final_message,verify_client_proof,verify_final_nonce,build_server_final_message,parse_scram_secret,mock_scram_secret,scram_mock_salt,pg_be_scram_build_secret,scram_verify_plain_password.src/backend/libpq/crypt.c—get_role_password,get_password_type,encrypt_password,md5_crypt_verify,plain_crypt_verify.src/include/libpq/protocol.h—AUTH_REQ_*상수,PqMsg_AuthenticationRequest,PqMsg_PasswordMessage,PqMsg_SASLResponse.src/include/common/scram-common.h—SCRAM_SHA_256_NAME,SCRAM_SHA_256_PLUS_NAME,SCRAM_RAW_NONCE_LEN, 키 길이 상수.
교재 장 (knowledge/research/dbms-general/ 아래)
섹션 제목: “교재 장 (knowledge/research/dbms-general/ 아래)”- Database System Concepts (Silberschatz et al.) — 접근 제어, 솔팅된 단방향 해시로 비밀번호 저장, 인증 vs. 인가.
- Architecture of a Database System (Hellerstein et al.), §“Process Models” / §“Admission Control” — 연결별 백엔드 설정에서 인증 핸드셰이크의 위치.
교차 참조 (형제 모듈 문서)
섹션 제목: “교차 참조 (형제 모듈 문서)”postgres-wire-protocol.md— FE/BE 프레이밍, 시작 핸드셰이크,sendAuthRequest의 위치,'R'/'p'메시지 타입.postgres-tls-gssapi.md— (계획됨)be-secure-openssl.c/be-secure-gssapi.c; 인증에 선행하고 채널 바인딩에be_tls_get_certificate_hash를 제공하는 TLS 설정; 디스패치의uaGSS/uaSSPIKerberos 분기.postgres-hooks.md—ClientAuthentication_hook을 포함한 훅 카탈로그.postgres-backend-lifecycle.md—postmaster→ 백엔드 시작 →InitPostgres에서ClientAuthentication에 도달하는 경로.