identity v3 WIP - #1576
identity v3 WIP#1576MichaelMure wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR is an “identity v3” work-in-progress that migrates identity signing and key representation away from embedded OpenPGP keys toward a pluggable signer model (GPG subprocess or SSH agent/SSHSIG) and W3C DID-style publicKeyMultibase public keys. It updates repository commit signing/verification and adjusts tests accordingly.
Changes:
- Introduces a
repository.Signerabstraction with SSH-agent-based SSHSIG signing and gpg-subprocess signing, plus SSHSIG verification helpers. - Updates commit signing APIs and commit signature payload representation (
[]byteinstead ofio.Reader) across repo implementations and tests. - Bumps identity format to v3 and changes key serialization to store
publicKeyMultibase(with legacy v2 key migration support).
Reviewed changes
Copilot reviewed 19 out of 20 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
repository/signer.go |
Adds Signer interface, SSH agent SSHSIG signing, GPG signing, and SSHSIG parsing/verification. |
repository/repo.go |
Switches signed commit API from OpenPGP entity to Signer; changes Commit fields to []byte. |
repository/gogit.go |
Implements new signing flow for go-git repo; re-encodes commit without signature to rebuild signed payload bytes. |
repository/mock_repo.go |
Updates mock repo signing to use Signer; returns signed payload/signature as []byte. |
repository/common.go |
Removes OpenPGP de-armoring helper no longer needed with armored signatures stored directly. |
repository/repo_testing.go |
Updates signature tests to use SSHSIG with an in-process SSH agent keyring. |
entity/dag/operation_pack.go |
Signs commits via Identity.Signer(); verifies SSHSIG or PGP armored detached signatures depending on signature type. |
entity/dag/operation_pack_test.go |
Updates signing test to use injected SSH-agent-backed signer. |
entities/identity/version.go |
Bumps identity version format to 3; changes key JSON handling; adds legacy v2 key migration path. |
entities/identity/key.go |
Reworks key model to publicKeyMultibase + origin (ssh/gpg) and signer construction. |
entities/identity/multibase.go / *_test.go |
Adds multibase encode/decode helpers and tests. |
entities/identity/interface.go / identity.go / stubs/tests |
Replaces SigningKey with Signer() and updates call sites and tests. |
go.mod / go.sum |
Adds/updates dependencies (including multibase/varint; also introduces several seemingly unused auth/web deps). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Signer returns a Signer for the first available signing key. | ||
| // Returns nil, nil if the identity has no keys. | ||
| func (i *Identity) Signer() (repository.Signer, error) { | ||
| for _, key := range i.Keys() { | ||
| s, err := key.Signer() | ||
| if err != nil { | ||
| return nil, err | ||
| continue | ||
| } | ||
| return key, nil | ||
| return s, nil | ||
| } | ||
| return nil, nil | ||
| } |
| multibase, err := pgpPubKeyToMultibase(entities[0]) | ||
| if err != nil { | ||
| return err | ||
| return nil, err | ||
| } | ||
|
|
||
| return repo.Keyring().Set(repository.Item{ | ||
| Key: k.public.KeyIdString(), | ||
| Data: buf.Bytes(), | ||
| }) | ||
| // pgpEntity is intentionally empty: v2 only stored the bare public key, not | ||
| // the full entity needed for signing. | ||
| return &Key{publicKeyMultibase: multibase, origin: KeyOriginGPG}, nil |
| if repository.IsSSHSignature(commit.Signature) { | ||
| var sshPubKeys []ssh.PublicKey | ||
| for _, key := range keys { | ||
| pub, err := key.SSHPublicKey() | ||
| if err == nil { | ||
| sshPubKeys = append(sshPubKeys, pub) | ||
| } | ||
| } | ||
| if err := repository.VerifySSHSIG(sshPubKeys, commit.SignedData, commit.Signature); err != nil { | ||
| return nil, fmt.Errorf("signature failure: %v", err) | ||
| } |
| func (g *GPGSigner) Sign(payload []byte) ([]byte, error) { | ||
| cmd := exec.Command("gpg", "--armor", "--detach-sign", "-u", g.fingerprint) | ||
| cmd.Stdin = bytes.NewReader(payload) | ||
| out, err := cmd.Output() | ||
| if err != nil { | ||
| return nil, fmt.Errorf("gpg sign: %w", err) | ||
| } | ||
| return out, nil | ||
| } |
| var blob blobFields | ||
| if err := ssh.Unmarshal(raw, &blob); err != nil { | ||
| return nil, "", nil, fmt.Errorf("unmarshal SSHSIG: %w", err) | ||
| } | ||
|
|
||
| pubKey, err = ssh.ParsePublicKey(blob.PublicKey) | ||
| if err != nil { | ||
| return nil, "", nil, fmt.Errorf("parse SSHSIG public key: %w", err) | ||
| } |
| github.com/fatih/color v1.17.0 | ||
| github.com/go-git/go-billy/v5 v5.6.2 | ||
| github.com/go-git/go-git/v5 v5.16.5 | ||
| github.com/golang-jwt/jwt/v5 v5.3.1 | ||
| github.com/gorilla/mux v1.8.1 | ||
| github.com/gorilla/websocket v1.5.3 | ||
| github.com/hashicorp/golang-lru/v2 v2.0.7 | ||
| github.com/icrowley/fake v0.0.0-20240710202011-f797eb4a99c0 | ||
| github.com/markbates/goth v1.82.0 | ||
| github.com/mattn/go-isatty v0.0.20 | ||
| github.com/multiformats/go-multibase v0.2.0 | ||
| github.com/multiformats/go-varint v0.0.7 |
| signedData := sshsigSignedData(sshsigNamespace, payload) | ||
| sig, err := ag.Sign(s.pubKey, signedData) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("SSH agent sign: %w", err) | ||
| } |
| func pubkeyMultibaseEncode(code uint64, keyBytes []byte) string { | ||
| payload := append(varint.ToUvarint(code), keyBytes...) | ||
| s, _ := mbase.Encode(mbase.Base58BTC, payload) | ||
| return s |
1243c94 to
8942220
Compare
|
@MichaelMure I'm interested in helping to get this through but don't have a ton of time. Are you OK if I spend some personal tokens helping with review using review-council? Sample output from another project where I've been working with the team: voxpupuli/openvox-ca#265 (comment) |
8942220 to
875cf79
Compare
| return | ||
| } | ||
|
|
||
| http.SetCookie(w, auth.SessionCookie(token, h.ttl)) |
|
|
||
| // Logout clears the session cookie. | ||
| func (h *Handler) Logout(w http.ResponseWriter, r *http.Request) { | ||
| http.SetCookie(w, auth.ExpiredCookie()) |
No description provided.