git 在 HTTPS 上怎么拿到密码?credential helper 到底收到什么、返回什么、什么时候被通知保存和删除?这篇用 git credential 命令直接驱动 helper,再在本机起一个要求认证的 smart-http 服务,把整个认证流程走一遍。
git credential fill 的输出(host 等原样回显,helper 补上了 username 和 password):
protocol=https
host=example.com
username=alice
password=<已隐藏>
helper 收到的调用(get)和它的 stdin:
[lab] get
stdin: protocol=https
stdin: host=example.com
helper 什么也不返回、又不允许提示终端时: 退出码 128;输出: fatal: could not read Username for 'https://example.com': terminal prompts disabledgit 与 helper 之间是极简的文本协议:stdin/stdout 上的 key=value 行,空行结束。helper 是 PATH 上的 git-credential-<名>。git credential fill 调用它的 get;没有 helper 能给出凭据又不允许提示终端时,fill 以 128 退出。
[lab] store
stdin: protocol=https
stdin: host=example.com
stdin: username=alice
stdin: password=<已隐藏>
[lab] erase
stdin: protocol=https
stdin: host=example.com
stdin: username=alice
stdin: password=<已隐藏>git credential approve 对应 helper 的 store(凭据可用,保存),reject 对应 erase(凭据被拒绝,删除)。get / store / erase 就是 helper 要实现的全部。
默认(不带 path):
[lab] get
stdin: protocol=https
stdin: host=git.example.com
stdin: username=bob
credential.useHttpPath=true 之后:
[lab] get
stdin: protocol=https
stdin: host=git.example.com
stdin: path=org/repo.git
stdin: username=boburl 被拆成 protocol、host、username 等字段;默认不传 path,credential.useHttpPath=true 才传,用来区分同一主机上的不同仓库。新版 git 还会传 capability[] 和服务器的 wwwauth[] 质询。
正确的凭据:git ls-remote 退出码 0
服务器看到的请求:
GET /repo.git/info/refs auth=无 要求认证
GET /repo.git/info/refs auth=有 通过
POST /repo.git/git-upload-pack auth=有 通过
helper 被调用:
[lab] get
stdin: capability[]=authtype
stdin: capability[]=state
stdin: protocol=http
stdin: host=127.0.0.1:9811
stdin: wwwauth[]=Basic realm="git"
[lab] store
stdin: protocol=http
stdin: host=127.0.0.1:9811
stdin: username=alice
stdin: password=<已隐藏>
helper 给了错误的密码:退出码 128;fatal: Authentication failed for 'http://127.0.0.1:9811/repo.git/'
服务器看到的请求:
GET /repo.git/info/refs auth=无 要求认证
GET /repo.git/info/refs auth=有 拒绝
helper 被调用(注意最后的 erase——认证被拒绝后 git 通知 helper 删掉这条凭据):
[lab] get
stdin: capability[]=authtype
stdin: capability[]=state
stdin: protocol=http
stdin: host=127.0.0.1:9811
stdin: wwwauth[]=Basic realm="git"
[lab] erase
stdin: protocol=http
stdin: host=127.0.0.1:9811
stdin: username=alice
stdin: password=<已隐藏>
stdin: wwwauth[]=Basic realm="git"git 第一个请求不带认证,服务器回 401 后 git 才调用 helper 的 get,拿到凭据重发请求;成功就 store,被拒绝就 erase 并报 Authentication failed。
第一个 helper(lab)没给凭据,第二个给了: protocol=https | host=example.com | username=备用用户 | password=<已隐藏>
[lab] get
stdin: protocol=https
stdin: host=example.com
[second] get
approve 时(store)两个 helper 都被通知:
[lab] store
stdin: protocol=https
stdin: host=example.com
stdin: username=备用用户
stdin: password=<已隐藏>
[second] store取凭据按配置顺序询问,第一个给出完整用户名和密码的赢;store 和 erase 通知所有 helper。
credential-store 写下的文件(明文!): https://alice:<密码>@example.com | 权限 600 之后 fill 不再需要提示: protocol=https | host=example.com | username=alice | password=<已隐藏> reject 之后文件内容: ''
credential-store 以明文把 https://用户:密码@主机 写进文件(权限 600);credential-cache 放内存里并设超时;macOS 的 osxkeychain 用钥匙串。协议相同,安全性不同。