detached 4269eaf

feat(proto): commit signature verification and trusted keys (#20)

commit
4269eaf418f570f2486a5005717ff5dae200e427
author
brice · · 2026-10-18 18:03 UTC
committer
bnema · · 2026-10-18 18:03 UTC
parent
e06a09c
signature
SSH UnverifiedSHA256:VOCA5lYARHcbKDZ7GhTV45VmcjWnJ6ZQIBazj17LivA The key is unknown
refs
tag: v0.6.0
7 files · +1099 -9
M README.md +1 -0
@@ -41,6 +41,7 @@ Status: early development.
4141 | `SecretService`, `VariableService`, `EnvironmentService` | CI settings |
4242 | `ArtifactService` | CI artifacts, for people |
4343 | `JobArtifactService` | CI artifacts v4, for runners |
44+| `TrustedKeyService` | SSH and OpenPGP keys commit signatures are checked against |
4445
4546 Every RPC: [docs/services.md](docs/services.md).
4647
@@ -41,6 +41,7 @@ Status: early development.
41 | `SecretService`, `VariableService`, `EnvironmentService` | CI settings |41 | `SecretService`, `VariableService`, `EnvironmentService` | CI settings |
42 | `ArtifactService` | CI artifacts, for people |42 | `ArtifactService` | CI artifacts, for people |
43 | `JobArtifactService` | CI artifacts v4, for runners |43 | `JobArtifactService` | CI artifacts v4, for runners |
44+| `TrustedKeyService` | SSH and OpenPGP keys commit signatures are checked against |
44 45
45 Every RPC: [docs/services.md](docs/services.md).46 Every RPC: [docs/services.md](docs/services.md).
46 47
M docs/services.md +1 -0
@@ -9,6 +9,7 @@
99 | `SessionService` | `CreateLoginLink`, `SignOutEverywhere` (owner only) |
1010 | `EventService` | `Watch` (streamed, resumable) |
1111 | `ServerService` | `GetVersion` (version and features), `GetSigningKey`, `RotateOIDCKey` (owner only) |
12+| `TrustedKeyService` | `ListTrustedKeys`, `AddTrustedKey` and `RemoveTrustedKey` (owner only). `Commit.verification` carries the result of the check (feature `commit_signatures`) |
1213 | `RunService` | `ListRuns`, `GetRun`, `GetJob`, `GetJobLog` (streamed, follow), `ListAnnotations`, `GetStepSummaries`, `CancelRun`, `RerunRun`, `DispatchWorkflow`, `ListChecks`, `ListRunners`, `RemoveRunner` |
1314 | `RunnerService` | `RegisterRunner`, `FetchJob` (long poll), `UpdateJob`, `AppendLog` (offset-idempotent), `Heartbeat`, `WatchJob` (cancel requests, streamed), `CompleteJob` (idempotent), `ResolveActions`, `UploadSummary` |
1415 | `SecretService` | `SetSecret`, `ListSecrets` (names only), `DeleteSecret`. Scopes: instance, repository, environment |
@@ -9,6 +9,7 @@
9 | `SessionService` | `CreateLoginLink`, `SignOutEverywhere` (owner only) |9 | `SessionService` | `CreateLoginLink`, `SignOutEverywhere` (owner only) |
10 | `EventService` | `Watch` (streamed, resumable) |10 | `EventService` | `Watch` (streamed, resumable) |
11 | `ServerService` | `GetVersion` (version and features), `GetSigningKey`, `RotateOIDCKey` (owner only) |11 | `ServerService` | `GetVersion` (version and features), `GetSigningKey`, `RotateOIDCKey` (owner only) |
12+| `TrustedKeyService` | `ListTrustedKeys`, `AddTrustedKey` and `RemoveTrustedKey` (owner only). `Commit.verification` carries the result of the check (feature `commit_signatures`) |
12 | `RunService` | `ListRuns`, `GetRun`, `GetJob`, `GetJobLog` (streamed, follow), `ListAnnotations`, `GetStepSummaries`, `CancelRun`, `RerunRun`, `DispatchWorkflow`, `ListChecks`, `ListRunners`, `RemoveRunner` |13 | `RunService` | `ListRuns`, `GetRun`, `GetJob`, `GetJobLog` (streamed, follow), `ListAnnotations`, `GetStepSummaries`, `CancelRun`, `RerunRun`, `DispatchWorkflow`, `ListChecks`, `ListRunners`, `RemoveRunner` |
13 | `RunnerService` | `RegisterRunner`, `FetchJob` (long poll), `UpdateJob`, `AppendLog` (offset-idempotent), `Heartbeat`, `WatchJob` (cancel requests, streamed), `CompleteJob` (idempotent), `ResolveActions`, `UploadSummary` |14 | `RunnerService` | `RegisterRunner`, `FetchJob` (long poll), `UpdateJob`, `AppendLog` (offset-idempotent), `Heartbeat`, `WatchJob` (cancel requests, streamed), `CompleteJob` (idempotent), `ResolveActions`, `UploadSummary` |
14 | `SecretService` | `SetSecret`, `ListSecrets` (names only), `DeleteSecret`. Scopes: instance, repository, environment |15 | `SecretService` | `SetSecret`, `ListSecrets` (names only), `DeleteSecret`. Scopes: instance, repository, environment |
A gen/ermite/ermiteconnect/signature.connect.go +183 -0
@@ -0,0 +1,183 @@
1+// SPDX-License-Identifier: MIT
2+
3+// Code generated by protoc-gen-connect-go. DO NOT EDIT.
4+//
5+// Source: ermite/signature.proto
6+
7+package ermiteconnect
8+
9+import (
10+ ermite "bnema.dev/ermite-api/gen/ermite"
11+ connect "connectrpc.com/connect"
12+ context "context"
13+ errors "errors"
14+ http "net/http"
15+ strings "strings"
16+)
17+
18+// This is a compile-time assertion to ensure that this generated file and the connect package are
19+// compatible. If you get a compiler error that this constant is not defined, this code was
20+// generated with a version of connect newer than the one compiled into your binary. You can fix the
21+// problem by either regenerating this code with an older version of connect or updating the connect
22+// version compiled into your binary.
23+const _ = connect.IsAtLeastVersion1_13_0
24+
25+const (
26+ // TrustedKeyServiceName is the fully-qualified name of the TrustedKeyService service.
27+ TrustedKeyServiceName = "ermite.TrustedKeyService"
28+)
29+
30+// These constants are the fully-qualified names of the RPCs defined in this package. They're
31+// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route.
32+//
33+// Note that these are different from the fully-qualified method names used by
34+// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to
35+// reflection-formatted method names, remove the leading slash and convert the remaining slash to a
36+// period.
37+const (
38+ // TrustedKeyServiceListTrustedKeysProcedure is the fully-qualified name of the TrustedKeyService's
39+ // ListTrustedKeys RPC.
40+ TrustedKeyServiceListTrustedKeysProcedure = "/ermite.TrustedKeyService/ListTrustedKeys"
41+ // TrustedKeyServiceAddTrustedKeyProcedure is the fully-qualified name of the TrustedKeyService's
42+ // AddTrustedKey RPC.
43+ TrustedKeyServiceAddTrustedKeyProcedure = "/ermite.TrustedKeyService/AddTrustedKey"
44+ // TrustedKeyServiceRemoveTrustedKeyProcedure is the fully-qualified name of the TrustedKeyService's
45+ // RemoveTrustedKey RPC.
46+ TrustedKeyServiceRemoveTrustedKeyProcedure = "/ermite.TrustedKeyService/RemoveTrustedKey"
47+)
48+
49+// TrustedKeyServiceClient is a client for the ermite.TrustedKeyService service.
50+type TrustedKeyServiceClient interface {
51+ // ListTrustedKeys lists the keys, the builtin key first. Any authenticated
52+ // caller may.
53+ ListTrustedKeys(context.Context, *connect.Request[ermite.ListTrustedKeysRequest]) (*connect.Response[ermite.ListTrustedKeysResponse], error)
54+ // AddTrustedKey adds a key. Owner only. invalid_argument for a key that
55+ // does not parse, a private key, no or too many emails; already_exists for
56+ // a key already listed; resource_exhausted beyond 100 keys.
57+ AddTrustedKey(context.Context, *connect.Request[ermite.AddTrustedKeyRequest]) (*connect.Response[ermite.AddTrustedKeyResponse], error)
58+ // RemoveTrustedKey removes a key. Owner only. not_found for an unknown id;
59+ // failed_precondition for the builtin key.
60+ RemoveTrustedKey(context.Context, *connect.Request[ermite.RemoveTrustedKeyRequest]) (*connect.Response[ermite.RemoveTrustedKeyResponse], error)
61+}
62+
63+// NewTrustedKeyServiceClient constructs a client for the ermite.TrustedKeyService service. By
64+// default, it uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses,
65+// and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the
66+// connect.WithGRPC() or connect.WithGRPCWeb() options.
67+//
68+// The URL supplied here should be the base URL for the Connect or gRPC server (for example,
69+// http://api.acme.com or https://acme.com/grpc).
70+func NewTrustedKeyServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) TrustedKeyServiceClient {
71+ baseURL = strings.TrimRight(baseURL, "/")
72+ trustedKeyServiceMethods := ermite.File_ermite_signature_proto.Services().ByName("TrustedKeyService").Methods()
73+ return &trustedKeyServiceClient{
74+ listTrustedKeys: connect.NewClient[ermite.ListTrustedKeysRequest, ermite.ListTrustedKeysResponse](
75+ httpClient,
76+ baseURL+TrustedKeyServiceListTrustedKeysProcedure,
77+ connect.WithSchema(trustedKeyServiceMethods.ByName("ListTrustedKeys")),
78+ connect.WithClientOptions(opts...),
79+ ),
80+ addTrustedKey: connect.NewClient[ermite.AddTrustedKeyRequest, ermite.AddTrustedKeyResponse](
81+ httpClient,
82+ baseURL+TrustedKeyServiceAddTrustedKeyProcedure,
83+ connect.WithSchema(trustedKeyServiceMethods.ByName("AddTrustedKey")),
84+ connect.WithClientOptions(opts...),
85+ ),
86+ removeTrustedKey: connect.NewClient[ermite.RemoveTrustedKeyRequest, ermite.RemoveTrustedKeyResponse](
87+ httpClient,
88+ baseURL+TrustedKeyServiceRemoveTrustedKeyProcedure,
89+ connect.WithSchema(trustedKeyServiceMethods.ByName("RemoveTrustedKey")),
90+ connect.WithClientOptions(opts...),
91+ ),
92+ }
93+}
94+
95+// trustedKeyServiceClient implements TrustedKeyServiceClient.
96+type trustedKeyServiceClient struct {
97+ listTrustedKeys *connect.Client[ermite.ListTrustedKeysRequest, ermite.ListTrustedKeysResponse]
98+ addTrustedKey *connect.Client[ermite.AddTrustedKeyRequest, ermite.AddTrustedKeyResponse]
99+ removeTrustedKey *connect.Client[ermite.RemoveTrustedKeyRequest, ermite.RemoveTrustedKeyResponse]
100+}
101+
102+// ListTrustedKeys calls ermite.TrustedKeyService.ListTrustedKeys.
103+func (c *trustedKeyServiceClient) ListTrustedKeys(ctx context.Context, req *connect.Request[ermite.ListTrustedKeysRequest]) (*connect.Response[ermite.ListTrustedKeysResponse], error) {
104+ return c.listTrustedKeys.CallUnary(ctx, req)
105+}
106+
107+// AddTrustedKey calls ermite.TrustedKeyService.AddTrustedKey.
108+func (c *trustedKeyServiceClient) AddTrustedKey(ctx context.Context, req *connect.Request[ermite.AddTrustedKeyRequest]) (*connect.Response[ermite.AddTrustedKeyResponse], error) {
109+ return c.addTrustedKey.CallUnary(ctx, req)
110+}
111+
112+// RemoveTrustedKey calls ermite.TrustedKeyService.RemoveTrustedKey.
113+func (c *trustedKeyServiceClient) RemoveTrustedKey(ctx context.Context, req *connect.Request[ermite.RemoveTrustedKeyRequest]) (*connect.Response[ermite.RemoveTrustedKeyResponse], error) {
114+ return c.removeTrustedKey.CallUnary(ctx, req)
115+}
116+
117+// TrustedKeyServiceHandler is an implementation of the ermite.TrustedKeyService service.
118+type TrustedKeyServiceHandler interface {
119+ // ListTrustedKeys lists the keys, the builtin key first. Any authenticated
120+ // caller may.
121+ ListTrustedKeys(context.Context, *connect.Request[ermite.ListTrustedKeysRequest]) (*connect.Response[ermite.ListTrustedKeysResponse], error)
122+ // AddTrustedKey adds a key. Owner only. invalid_argument for a key that
123+ // does not parse, a private key, no or too many emails; already_exists for
124+ // a key already listed; resource_exhausted beyond 100 keys.
125+ AddTrustedKey(context.Context, *connect.Request[ermite.AddTrustedKeyRequest]) (*connect.Response[ermite.AddTrustedKeyResponse], error)
126+ // RemoveTrustedKey removes a key. Owner only. not_found for an unknown id;
127+ // failed_precondition for the builtin key.
128+ RemoveTrustedKey(context.Context, *connect.Request[ermite.RemoveTrustedKeyRequest]) (*connect.Response[ermite.RemoveTrustedKeyResponse], error)
129+}
130+
131+// NewTrustedKeyServiceHandler builds an HTTP handler from the service implementation. It returns
132+// the path on which to mount the handler and the handler itself.
133+//
134+// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf
135+// and JSON codecs. They also support gzip compression.
136+func NewTrustedKeyServiceHandler(svc TrustedKeyServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) {
137+ trustedKeyServiceMethods := ermite.File_ermite_signature_proto.Services().ByName("TrustedKeyService").Methods()
138+ trustedKeyServiceListTrustedKeysHandler := connect.NewUnaryHandler(
139+ TrustedKeyServiceListTrustedKeysProcedure,
140+ svc.ListTrustedKeys,
141+ connect.WithSchema(trustedKeyServiceMethods.ByName("ListTrustedKeys")),
142+ connect.WithHandlerOptions(opts...),
143+ )
144+ trustedKeyServiceAddTrustedKeyHandler := connect.NewUnaryHandler(
145+ TrustedKeyServiceAddTrustedKeyProcedure,
146+ svc.AddTrustedKey,
147+ connect.WithSchema(trustedKeyServiceMethods.ByName("AddTrustedKey")),
148+ connect.WithHandlerOptions(opts...),
149+ )
150+ trustedKeyServiceRemoveTrustedKeyHandler := connect.NewUnaryHandler(
151+ TrustedKeyServiceRemoveTrustedKeyProcedure,
152+ svc.RemoveTrustedKey,
153+ connect.WithSchema(trustedKeyServiceMethods.ByName("RemoveTrustedKey")),
154+ connect.WithHandlerOptions(opts...),
155+ )
156+ return "/ermite.TrustedKeyService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
157+ switch r.URL.Path {
158+ case TrustedKeyServiceListTrustedKeysProcedure:
159+ trustedKeyServiceListTrustedKeysHandler.ServeHTTP(w, r)
160+ case TrustedKeyServiceAddTrustedKeyProcedure:
161+ trustedKeyServiceAddTrustedKeyHandler.ServeHTTP(w, r)
162+ case TrustedKeyServiceRemoveTrustedKeyProcedure:
163+ trustedKeyServiceRemoveTrustedKeyHandler.ServeHTTP(w, r)
164+ default:
165+ http.NotFound(w, r)
166+ }
167+ })
168+}
169+
170+// UnimplementedTrustedKeyServiceHandler returns CodeUnimplemented from all methods.
171+type UnimplementedTrustedKeyServiceHandler struct{}
172+
173+func (UnimplementedTrustedKeyServiceHandler) ListTrustedKeys(context.Context, *connect.Request[ermite.ListTrustedKeysRequest]) (*connect.Response[ermite.ListTrustedKeysResponse], error) {
174+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.TrustedKeyService.ListTrustedKeys is not implemented"))
175+}
176+
177+func (UnimplementedTrustedKeyServiceHandler) AddTrustedKey(context.Context, *connect.Request[ermite.AddTrustedKeyRequest]) (*connect.Response[ermite.AddTrustedKeyResponse], error) {
178+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.TrustedKeyService.AddTrustedKey is not implemented"))
179+}
180+
181+func (UnimplementedTrustedKeyServiceHandler) RemoveTrustedKey(context.Context, *connect.Request[ermite.RemoveTrustedKeyRequest]) (*connect.Response[ermite.RemoveTrustedKeyResponse], error) {
182+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.TrustedKeyService.RemoveTrustedKey is not implemented"))
183+}
@@ -0,0 +1,183 @@
1+// SPDX-License-Identifier: MIT
2+
3+// Code generated by protoc-gen-connect-go. DO NOT EDIT.
4+//
5+// Source: ermite/signature.proto
6+
7+package ermiteconnect
8+
9+import (
10+ ermite "bnema.dev/ermite-api/gen/ermite"
11+ connect "connectrpc.com/connect"
12+ context "context"
13+ errors "errors"
14+ http "net/http"
15+ strings "strings"
16+)
17+
18+// This is a compile-time assertion to ensure that this generated file and the connect package are
19+// compatible. If you get a compiler error that this constant is not defined, this code was
20+// generated with a version of connect newer than the one compiled into your binary. You can fix the
21+// problem by either regenerating this code with an older version of connect or updating the connect
22+// version compiled into your binary.
23+const _ = connect.IsAtLeastVersion1_13_0
24+
25+const (
26+ // TrustedKeyServiceName is the fully-qualified name of the TrustedKeyService service.
27+ TrustedKeyServiceName = "ermite.TrustedKeyService"
28+)
29+
30+// These constants are the fully-qualified names of the RPCs defined in this package. They're
31+// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route.
32+//
33+// Note that these are different from the fully-qualified method names used by
34+// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to
35+// reflection-formatted method names, remove the leading slash and convert the remaining slash to a
36+// period.
37+const (
38+ // TrustedKeyServiceListTrustedKeysProcedure is the fully-qualified name of the TrustedKeyService's
39+ // ListTrustedKeys RPC.
40+ TrustedKeyServiceListTrustedKeysProcedure = "/ermite.TrustedKeyService/ListTrustedKeys"
41+ // TrustedKeyServiceAddTrustedKeyProcedure is the fully-qualified name of the TrustedKeyService's
42+ // AddTrustedKey RPC.
43+ TrustedKeyServiceAddTrustedKeyProcedure = "/ermite.TrustedKeyService/AddTrustedKey"
44+ // TrustedKeyServiceRemoveTrustedKeyProcedure is the fully-qualified name of the TrustedKeyService's
45+ // RemoveTrustedKey RPC.
46+ TrustedKeyServiceRemoveTrustedKeyProcedure = "/ermite.TrustedKeyService/RemoveTrustedKey"
47+)
48+
49+// TrustedKeyServiceClient is a client for the ermite.TrustedKeyService service.
50+type TrustedKeyServiceClient interface {
51+ // ListTrustedKeys lists the keys, the builtin key first. Any authenticated
52+ // caller may.
53+ ListTrustedKeys(context.Context, *connect.Request[ermite.ListTrustedKeysRequest]) (*connect.Response[ermite.ListTrustedKeysResponse], error)
54+ // AddTrustedKey adds a key. Owner only. invalid_argument for a key that
55+ // does not parse, a private key, no or too many emails; already_exists for
56+ // a key already listed; resource_exhausted beyond 100 keys.
57+ AddTrustedKey(context.Context, *connect.Request[ermite.AddTrustedKeyRequest]) (*connect.Response[ermite.AddTrustedKeyResponse], error)
58+ // RemoveTrustedKey removes a key. Owner only. not_found for an unknown id;
59+ // failed_precondition for the builtin key.
60+ RemoveTrustedKey(context.Context, *connect.Request[ermite.RemoveTrustedKeyRequest]) (*connect.Response[ermite.RemoveTrustedKeyResponse], error)
61+}
62+
63+// NewTrustedKeyServiceClient constructs a client for the ermite.TrustedKeyService service. By
64+// default, it uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses,
65+// and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the
66+// connect.WithGRPC() or connect.WithGRPCWeb() options.
67+//
68+// The URL supplied here should be the base URL for the Connect or gRPC server (for example,
69+// http://api.acme.com or https://acme.com/grpc).
70+func NewTrustedKeyServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) TrustedKeyServiceClient {
71+ baseURL = strings.TrimRight(baseURL, "/")
72+ trustedKeyServiceMethods := ermite.File_ermite_signature_proto.Services().ByName("TrustedKeyService").Methods()
73+ return &trustedKeyServiceClient{
74+ listTrustedKeys: connect.NewClient[ermite.ListTrustedKeysRequest, ermite.ListTrustedKeysResponse](
75+ httpClient,
76+ baseURL+TrustedKeyServiceListTrustedKeysProcedure,
77+ connect.WithSchema(trustedKeyServiceMethods.ByName("ListTrustedKeys")),
78+ connect.WithClientOptions(opts...),
79+ ),
80+ addTrustedKey: connect.NewClient[ermite.AddTrustedKeyRequest, ermite.AddTrustedKeyResponse](
81+ httpClient,
82+ baseURL+TrustedKeyServiceAddTrustedKeyProcedure,
83+ connect.WithSchema(trustedKeyServiceMethods.ByName("AddTrustedKey")),
84+ connect.WithClientOptions(opts...),
85+ ),
86+ removeTrustedKey: connect.NewClient[ermite.RemoveTrustedKeyRequest, ermite.RemoveTrustedKeyResponse](
87+ httpClient,
88+ baseURL+TrustedKeyServiceRemoveTrustedKeyProcedure,
89+ connect.WithSchema(trustedKeyServiceMethods.ByName("RemoveTrustedKey")),
90+ connect.WithClientOptions(opts...),
91+ ),
92+ }
93+}
94+
95+// trustedKeyServiceClient implements TrustedKeyServiceClient.
96+type trustedKeyServiceClient struct {
97+ listTrustedKeys *connect.Client[ermite.ListTrustedKeysRequest, ermite.ListTrustedKeysResponse]
98+ addTrustedKey *connect.Client[ermite.AddTrustedKeyRequest, ermite.AddTrustedKeyResponse]
99+ removeTrustedKey *connect.Client[ermite.RemoveTrustedKeyRequest, ermite.RemoveTrustedKeyResponse]
100+}
101+
102+// ListTrustedKeys calls ermite.TrustedKeyService.ListTrustedKeys.
103+func (c *trustedKeyServiceClient) ListTrustedKeys(ctx context.Context, req *connect.Request[ermite.ListTrustedKeysRequest]) (*connect.Response[ermite.ListTrustedKeysResponse], error) {
104+ return c.listTrustedKeys.CallUnary(ctx, req)
105+}
106+
107+// AddTrustedKey calls ermite.TrustedKeyService.AddTrustedKey.
108+func (c *trustedKeyServiceClient) AddTrustedKey(ctx context.Context, req *connect.Request[ermite.AddTrustedKeyRequest]) (*connect.Response[ermite.AddTrustedKeyResponse], error) {
109+ return c.addTrustedKey.CallUnary(ctx, req)
110+}
111+
112+// RemoveTrustedKey calls ermite.TrustedKeyService.RemoveTrustedKey.
113+func (c *trustedKeyServiceClient) RemoveTrustedKey(ctx context.Context, req *connect.Request[ermite.RemoveTrustedKeyRequest]) (*connect.Response[ermite.RemoveTrustedKeyResponse], error) {
114+ return c.removeTrustedKey.CallUnary(ctx, req)
115+}
116+
117+// TrustedKeyServiceHandler is an implementation of the ermite.TrustedKeyService service.
118+type TrustedKeyServiceHandler interface {
119+ // ListTrustedKeys lists the keys, the builtin key first. Any authenticated
120+ // caller may.
121+ ListTrustedKeys(context.Context, *connect.Request[ermite.ListTrustedKeysRequest]) (*connect.Response[ermite.ListTrustedKeysResponse], error)
122+ // AddTrustedKey adds a key. Owner only. invalid_argument for a key that
123+ // does not parse, a private key, no or too many emails; already_exists for
124+ // a key already listed; resource_exhausted beyond 100 keys.
125+ AddTrustedKey(context.Context, *connect.Request[ermite.AddTrustedKeyRequest]) (*connect.Response[ermite.AddTrustedKeyResponse], error)
126+ // RemoveTrustedKey removes a key. Owner only. not_found for an unknown id;
127+ // failed_precondition for the builtin key.
128+ RemoveTrustedKey(context.Context, *connect.Request[ermite.RemoveTrustedKeyRequest]) (*connect.Response[ermite.RemoveTrustedKeyResponse], error)
129+}
130+
131+// NewTrustedKeyServiceHandler builds an HTTP handler from the service implementation. It returns
132+// the path on which to mount the handler and the handler itself.
133+//
134+// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf
135+// and JSON codecs. They also support gzip compression.
136+func NewTrustedKeyServiceHandler(svc TrustedKeyServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) {
137+ trustedKeyServiceMethods := ermite.File_ermite_signature_proto.Services().ByName("TrustedKeyService").Methods()
138+ trustedKeyServiceListTrustedKeysHandler := connect.NewUnaryHandler(
139+ TrustedKeyServiceListTrustedKeysProcedure,
140+ svc.ListTrustedKeys,
141+ connect.WithSchema(trustedKeyServiceMethods.ByName("ListTrustedKeys")),
142+ connect.WithHandlerOptions(opts...),
143+ )
144+ trustedKeyServiceAddTrustedKeyHandler := connect.NewUnaryHandler(
145+ TrustedKeyServiceAddTrustedKeyProcedure,
146+ svc.AddTrustedKey,
147+ connect.WithSchema(trustedKeyServiceMethods.ByName("AddTrustedKey")),
148+ connect.WithHandlerOptions(opts...),
149+ )
150+ trustedKeyServiceRemoveTrustedKeyHandler := connect.NewUnaryHandler(
151+ TrustedKeyServiceRemoveTrustedKeyProcedure,
152+ svc.RemoveTrustedKey,
153+ connect.WithSchema(trustedKeyServiceMethods.ByName("RemoveTrustedKey")),
154+ connect.WithHandlerOptions(opts...),
155+ )
156+ return "/ermite.TrustedKeyService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
157+ switch r.URL.Path {
158+ case TrustedKeyServiceListTrustedKeysProcedure:
159+ trustedKeyServiceListTrustedKeysHandler.ServeHTTP(w, r)
160+ case TrustedKeyServiceAddTrustedKeyProcedure:
161+ trustedKeyServiceAddTrustedKeyHandler.ServeHTTP(w, r)
162+ case TrustedKeyServiceRemoveTrustedKeyProcedure:
163+ trustedKeyServiceRemoveTrustedKeyHandler.ServeHTTP(w, r)
164+ default:
165+ http.NotFound(w, r)
166+ }
167+ })
168+}
169+
170+// UnimplementedTrustedKeyServiceHandler returns CodeUnimplemented from all methods.
171+type UnimplementedTrustedKeyServiceHandler struct{}
172+
173+func (UnimplementedTrustedKeyServiceHandler) ListTrustedKeys(context.Context, *connect.Request[ermite.ListTrustedKeysRequest]) (*connect.Response[ermite.ListTrustedKeysResponse], error) {
174+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.TrustedKeyService.ListTrustedKeys is not implemented"))
175+}
176+
177+func (UnimplementedTrustedKeyServiceHandler) AddTrustedKey(context.Context, *connect.Request[ermite.AddTrustedKeyRequest]) (*connect.Response[ermite.AddTrustedKeyResponse], error) {
178+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.TrustedKeyService.AddTrustedKey is not implemented"))
179+}
180+
181+func (UnimplementedTrustedKeyServiceHandler) RemoveTrustedKey(context.Context, *connect.Request[ermite.RemoveTrustedKeyRequest]) (*connect.Response[ermite.RemoveTrustedKeyResponse], error) {
182+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.TrustedKeyService.RemoveTrustedKey is not implemented"))
183+}
A gen/ermite/signature.pb.go +759 -0 generated file: collapsed
M gen/ermite/types.pb.go +23 -9
@@ -169,7 +169,10 @@ type Commit struct {
169169 Summary string `protobuf:"bytes,5,opt,name=summary,proto3" json:"summary,omitempty"`
170170 // Message is the full commit message, including the summary. Text that is
171171 // not valid UTF-8 is converted with the replacement character.
172- Message string `protobuf:"bytes,6,opt,name=message,proto3" json:"message,omitempty"`
172+ Message string `protobuf:"bytes,6,opt,name=message,proto3" json:"message,omitempty"`
173+ // Verification tells whether and how the commit is signed. Unset on
174+ // servers without the "commit_signatures" feature.
175+ Verification *CommitVerification `protobuf:"bytes,7,opt,name=verification,proto3" json:"verification,omitempty"`
173176 unknownFields protoimpl.UnknownFields
174177 sizeCache protoimpl.SizeCache
175178 }
@@ -246,15 +249,22 @@ func (x *Commit) GetMessage() string {
246249 return ""
247250 }
248251
252+func (x *Commit) GetVerification() *CommitVerification {
253+ if x != nil {
254+ return x.Verification
255+ }
256+ return nil
257+}
258+
249259 var File_ermite_types_proto protoreflect.FileDescriptor
250260
251261 const file_ermite_types_proto_rawDesc = "" +
252262 "\n" +
253- "\x12ermite/types.proto\x12\x06ermite\x1a\x1fgoogle/protobuf/timestamp.proto\"e\n" +
263+ "\x12ermite/types.proto\x12\x06ermite\x1a\x16ermite/signature.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"e\n" +
254264 "\tSignature\x12\x12\n" +
255265 "\x04name\x18\x01 \x01(\tR\x04name\x12\x14\n" +
256266 "\x05email\x18\x02 \x01(\tR\x05email\x12.\n" +
257- "\x04time\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\x04time\"\xc7\x01\n" +
267+ "\x04time\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\x04time\"\x87\x02\n" +
258268 "\x06Commit\x12\x0e\n" +
259269 "\x02id\x18\x01 \x01(\tR\x02id\x12\x1d\n" +
260270 "\n" +
@@ -262,7 +272,8 @@ const file_ermite_types_proto_rawDesc = "" +
262272 "\x06author\x18\x03 \x01(\v2\x11.ermite.SignatureR\x06author\x12/\n" +
263273 "\tcommitter\x18\x04 \x01(\v2\x11.ermite.SignatureR\tcommitter\x12\x18\n" +
264274 "\asummary\x18\x05 \x01(\tR\asummary\x12\x18\n" +
265- "\amessage\x18\x06 \x01(\tR\amessage*[\n" +
275+ "\amessage\x18\x06 \x01(\tR\amessage\x12>\n" +
276+ "\fverification\x18\a \x01(\v2\x1a.ermite.CommitVerificationR\fverification*[\n" +
266277 "\tMergeMode\x12\x1a\n" +
267278 "\x16MERGE_MODE_UNSPECIFIED\x10\x00\x12\x1b\n" +
268279 "\x17MERGE_MODE_FAST_FORWARD\x10\x01\x12\x15\n" +
@@ -290,16 +301,18 @@ var file_ermite_types_proto_goTypes = []any{
290301 (*Signature)(nil), // 1: ermite.Signature
291302 (*Commit)(nil), // 2: ermite.Commit
292303 (*timestamppb.Timestamp)(nil), // 3: google.protobuf.Timestamp
304+ (*CommitVerification)(nil), // 4: ermite.CommitVerification
293305 }
294306 var file_ermite_types_proto_depIdxs = []int32{
295307 3, // 0: ermite.Signature.time:type_name -> google.protobuf.Timestamp
296308 1, // 1: ermite.Commit.author:type_name -> ermite.Signature
297309 1, // 2: ermite.Commit.committer:type_name -> ermite.Signature
298- 3, // [3:3] is the sub-list for method output_type
299- 3, // [3:3] is the sub-list for method input_type
300- 3, // [3:3] is the sub-list for extension type_name
301- 3, // [3:3] is the sub-list for extension extendee
302- 0, // [0:3] is the sub-list for field type_name
310+ 4, // 3: ermite.Commit.verification:type_name -> ermite.CommitVerification
311+ 4, // [4:4] is the sub-list for method output_type
312+ 4, // [4:4] is the sub-list for method input_type
313+ 4, // [4:4] is the sub-list for extension type_name
314+ 4, // [4:4] is the sub-list for extension extendee
315+ 0, // [0:4] is the sub-list for field type_name
303316 }
304317
305318 func init() { file_ermite_types_proto_init() }
@@ -307,6 +320,7 @@ func file_ermite_types_proto_init() {
307320 if File_ermite_types_proto != nil {
308321 return
309322 }
323+ file_ermite_signature_proto_init()
310324 type x struct{}
311325 out := protoimpl.TypeBuilder{
312326 File: protoimpl.DescBuilder{
@@ -169,7 +169,10 @@ type Commit struct {
169 Summary string `protobuf:"bytes,5,opt,name=summary,proto3" json:"summary,omitempty"`169 Summary string `protobuf:"bytes,5,opt,name=summary,proto3" json:"summary,omitempty"`
170 // Message is the full commit message, including the summary. Text that is170 // Message is the full commit message, including the summary. Text that is
171 // not valid UTF-8 is converted with the replacement character.171 // not valid UTF-8 is converted with the replacement character.
172- Message string `protobuf:"bytes,6,opt,name=message,proto3" json:"message,omitempty"`172+ Message string `protobuf:"bytes,6,opt,name=message,proto3" json:"message,omitempty"`
173+ // Verification tells whether and how the commit is signed. Unset on
174+ // servers without the "commit_signatures" feature.
175+ Verification *CommitVerification `protobuf:"bytes,7,opt,name=verification,proto3" json:"verification,omitempty"`
173 unknownFields protoimpl.UnknownFields176 unknownFields protoimpl.UnknownFields
174 sizeCache protoimpl.SizeCache177 sizeCache protoimpl.SizeCache
175 }178 }
@@ -246,15 +249,22 @@ func (x *Commit) GetMessage() string {
246 return ""249 return ""
247 }250 }
248 251
252+func (x *Commit) GetVerification() *CommitVerification {
253+ if x != nil {
254+ return x.Verification
255+ }
256+ return nil
257+}
258+
249 var File_ermite_types_proto protoreflect.FileDescriptor259 var File_ermite_types_proto protoreflect.FileDescriptor
250 260
251 const file_ermite_types_proto_rawDesc = "" +261 const file_ermite_types_proto_rawDesc = "" +
252 "\n" +262 "\n" +
253- "\x12ermite/types.proto\x12\x06ermite\x1a\x1fgoogle/protobuf/timestamp.proto\"e\n" +263+ "\x12ermite/types.proto\x12\x06ermite\x1a\x16ermite/signature.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"e\n" +
254 "\tSignature\x12\x12\n" +264 "\tSignature\x12\x12\n" +
255 "\x04name\x18\x01 \x01(\tR\x04name\x12\x14\n" +265 "\x04name\x18\x01 \x01(\tR\x04name\x12\x14\n" +
256 "\x05email\x18\x02 \x01(\tR\x05email\x12.\n" +266 "\x05email\x18\x02 \x01(\tR\x05email\x12.\n" +
257- "\x04time\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\x04time\"\xc7\x01\n" +267+ "\x04time\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\x04time\"\x87\x02\n" +
258 "\x06Commit\x12\x0e\n" +268 "\x06Commit\x12\x0e\n" +
259 "\x02id\x18\x01 \x01(\tR\x02id\x12\x1d\n" +269 "\x02id\x18\x01 \x01(\tR\x02id\x12\x1d\n" +
260 "\n" +270 "\n" +
@@ -262,7 +272,8 @@ const file_ermite_types_proto_rawDesc = "" +
262 "\x06author\x18\x03 \x01(\v2\x11.ermite.SignatureR\x06author\x12/\n" +272 "\x06author\x18\x03 \x01(\v2\x11.ermite.SignatureR\x06author\x12/\n" +
263 "\tcommitter\x18\x04 \x01(\v2\x11.ermite.SignatureR\tcommitter\x12\x18\n" +273 "\tcommitter\x18\x04 \x01(\v2\x11.ermite.SignatureR\tcommitter\x12\x18\n" +
264 "\asummary\x18\x05 \x01(\tR\asummary\x12\x18\n" +274 "\asummary\x18\x05 \x01(\tR\asummary\x12\x18\n" +
265- "\amessage\x18\x06 \x01(\tR\amessage*[\n" +275+ "\amessage\x18\x06 \x01(\tR\amessage\x12>\n" +
276+ "\fverification\x18\a \x01(\v2\x1a.ermite.CommitVerificationR\fverification*[\n" +
266 "\tMergeMode\x12\x1a\n" +277 "\tMergeMode\x12\x1a\n" +
267 "\x16MERGE_MODE_UNSPECIFIED\x10\x00\x12\x1b\n" +278 "\x16MERGE_MODE_UNSPECIFIED\x10\x00\x12\x1b\n" +
268 "\x17MERGE_MODE_FAST_FORWARD\x10\x01\x12\x15\n" +279 "\x17MERGE_MODE_FAST_FORWARD\x10\x01\x12\x15\n" +
@@ -290,16 +301,18 @@ var file_ermite_types_proto_goTypes = []any{
290 (*Signature)(nil), // 1: ermite.Signature301 (*Signature)(nil), // 1: ermite.Signature
291 (*Commit)(nil), // 2: ermite.Commit302 (*Commit)(nil), // 2: ermite.Commit
292 (*timestamppb.Timestamp)(nil), // 3: google.protobuf.Timestamp303 (*timestamppb.Timestamp)(nil), // 3: google.protobuf.Timestamp
304+ (*CommitVerification)(nil), // 4: ermite.CommitVerification
293 }305 }
294 var file_ermite_types_proto_depIdxs = []int32{306 var file_ermite_types_proto_depIdxs = []int32{
295 3, // 0: ermite.Signature.time:type_name -> google.protobuf.Timestamp307 3, // 0: ermite.Signature.time:type_name -> google.protobuf.Timestamp
296 1, // 1: ermite.Commit.author:type_name -> ermite.Signature308 1, // 1: ermite.Commit.author:type_name -> ermite.Signature
297 1, // 2: ermite.Commit.committer:type_name -> ermite.Signature309 1, // 2: ermite.Commit.committer:type_name -> ermite.Signature
298- 3, // [3:3] is the sub-list for method output_type310+ 4, // 3: ermite.Commit.verification:type_name -> ermite.CommitVerification
299- 3, // [3:3] is the sub-list for method input_type311+ 4, // [4:4] is the sub-list for method output_type
300- 3, // [3:3] is the sub-list for extension type_name312+ 4, // [4:4] is the sub-list for method input_type
301- 3, // [3:3] is the sub-list for extension extendee313+ 4, // [4:4] is the sub-list for extension type_name
302- 0, // [0:3] is the sub-list for field type_name314+ 4, // [4:4] is the sub-list for extension extendee
315+ 0, // [0:4] is the sub-list for field type_name
303 }316 }
304 317
305 func init() { file_ermite_types_proto_init() }318 func init() { file_ermite_types_proto_init() }
@@ -307,6 +320,7 @@ func file_ermite_types_proto_init() {
307 if File_ermite_types_proto != nil {320 if File_ermite_types_proto != nil {
308 return321 return
309 }322 }
323+ file_ermite_signature_proto_init()
310 type x struct{}324 type x struct{}
311 out := protoimpl.TypeBuilder{325 out := protoimpl.TypeBuilder{
312 File: protoimpl.DescBuilder{326 File: protoimpl.DescBuilder{
A proto/ermite/signature.proto +128 -0
@@ -0,0 +1,128 @@
1+// SPDX-License-Identifier: MIT
2+
3+syntax = "proto3";
4+
5+package ermite;
6+
7+import "google/protobuf/timestamp.proto";
8+
9+// SignatureFormat is the protocol of a commit signature.
10+enum SignatureFormat {
11+ // Not signed, or not set.
12+ SIGNATURE_FORMAT_UNSPECIFIED = 0;
13+ // Git SSH signature (gpg.format=ssh, "-----BEGIN SSH SIGNATURE-----").
14+ SIGNATURE_FORMAT_SSH = 1;
15+ // OpenPGP signature (gpg.format=openpgp, "-----BEGIN PGP SIGNATURE-----").
16+ SIGNATURE_FORMAT_GPG = 2;
17+ // X.509 (gpgsm) signature. Recognized, never verified.
18+ SIGNATURE_FORMAT_X509 = 3;
19+}
20+
21+// SignatureStatus is the outcome of checking the signature of a commit.
22+enum SignatureStatus {
23+ // The server did not check the commit (it could not be read).
24+ SIGNATURE_STATUS_UNSPECIFIED = 0;
25+ // The commit carries no signature.
26+ SIGNATURE_STATUS_UNSIGNED = 1;
27+ // Valid signature by a trusted key that is trusted for the committer email.
28+ SIGNATURE_STATUS_VERIFIED = 2;
29+ // Well-formed signature by a key the server does not trust.
30+ SIGNATURE_STATUS_UNKNOWN_KEY = 3;
31+ // The signature is malformed, does not match the commit, or uses a weak
32+ // hash (SHA-1 and older).
33+ SIGNATURE_STATUS_BAD_SIGNATURE = 4;
34+ // Valid signature by a trusted key that is not trusted for the committer
35+ // email.
36+ SIGNATURE_STATUS_EMAIL_MISMATCH = 5;
37+ // The OpenPGP key or signature had expired when the signature was made.
38+ SIGNATURE_STATUS_EXPIRED = 6;
39+ // The OpenPGP key is revoked.
40+ SIGNATURE_STATUS_REVOKED = 7;
41+ // The format is not verified by the server (X.509).
42+ SIGNATURE_STATUS_UNSUPPORTED = 8;
43+}
44+
45+// CommitVerification tells whether and how a commit is signed.
46+message CommitVerification {
47+ SignatureFormat format = 1;
48+ SignatureStatus status = 2;
49+ // Fingerprint of the signing key when the signature names one:
50+ // "SHA256:<base64>" for SSH, upper-case hex for OpenPGP (the 16-digit
51+ // issuer key id when the key is unknown).
52+ string fingerprint = 3;
53+ // KeyId is the id of the trusted key that matched; empty when none did.
54+ string key_id = 4;
55+ // KeyTitle is the title of the trusted key that matched.
56+ string key_title = 5;
57+}
58+
59+// TrustedKey is a public key whose signatures the server accepts for the
60+// listed committer emails.
61+message TrustedKey {
62+ // Id is the key fingerprint: "SHA256:<base64>" for SSH, the primary key
63+ // fingerprint in upper-case hex for OpenPGP.
64+ string id = 1;
65+ // Format is SIGNATURE_FORMAT_SSH or SIGNATURE_FORMAT_GPG.
66+ SignatureFormat format = 2;
67+ string title = 3;
68+ // PublicKey is "<type> <base64>" for SSH, the armored public key for
69+ // OpenPGP.
70+ string public_key = 4;
71+ // Emails are the committer emails the key may sign for, lower-case.
72+ repeated string emails = 5;
73+ google.protobuf.Timestamp created_at = 6;
74+ // Builtin is true for the server's own signing key
75+ // (ServerService.GetSigningKey): always trusted, cannot be removed.
76+ bool builtin = 7;
77+}
78+
79+// TrustedKeyService manages the keys commit signatures are checked against.
80+// Servers that implement it list "commit_signatures" in
81+// GetVersionResponse.features; their Commit messages then carry
82+// verification.
83+service TrustedKeyService {
84+ // ListTrustedKeys lists the keys, the builtin key first. Any authenticated
85+ // caller may.
86+ rpc ListTrustedKeys(ListTrustedKeysRequest) returns (ListTrustedKeysResponse);
87+ // AddTrustedKey adds a key. Owner only. invalid_argument for a key that
88+ // does not parse, a private key, no or too many emails; already_exists for
89+ // a key already listed; resource_exhausted beyond 100 keys.
90+ rpc AddTrustedKey(AddTrustedKeyRequest) returns (AddTrustedKeyResponse);
91+ // RemoveTrustedKey removes a key. Owner only. not_found for an unknown id;
92+ // failed_precondition for the builtin key.
93+ rpc RemoveTrustedKey(RemoveTrustedKeyRequest) returns (RemoveTrustedKeyResponse);
94+}
95+
96+// ListTrustedKeysRequest is empty.
97+message ListTrustedKeysRequest {}
98+
99+// ListTrustedKeysResponse lists every trusted key.
100+message ListTrustedKeysResponse {
101+ repeated TrustedKey keys = 1;
102+}
103+
104+// AddTrustedKeyRequest describes the key to trust.
105+message AddTrustedKeyRequest {
106+ // Format is SIGNATURE_FORMAT_SSH or SIGNATURE_FORMAT_GPG.
107+ SignatureFormat format = 1;
108+ // Title is a label, 1 to 100 bytes.
109+ string title = 2;
110+ // PublicKey is an authorized_keys line for SSH, or an armored public key
111+ // block for OpenPGP (at most 64 KiB).
112+ string public_key = 3;
113+ // Emails are the committer emails the key may sign for, 1 to 10.
114+ repeated string emails = 4;
115+}
116+
117+// AddTrustedKeyResponse returns the stored key.
118+message AddTrustedKeyResponse {
119+ TrustedKey key = 1;
120+}
121+
122+// RemoveTrustedKeyRequest names the key to remove.
123+message RemoveTrustedKeyRequest {
124+ string id = 1;
125+}
126+
127+// RemoveTrustedKeyResponse is empty.
128+message RemoveTrustedKeyResponse {}
@@ -0,0 +1,128 @@
1+// SPDX-License-Identifier: MIT
2+
3+syntax = "proto3";
4+
5+package ermite;
6+
7+import "google/protobuf/timestamp.proto";
8+
9+// SignatureFormat is the protocol of a commit signature.
10+enum SignatureFormat {
11+ // Not signed, or not set.
12+ SIGNATURE_FORMAT_UNSPECIFIED = 0;
13+ // Git SSH signature (gpg.format=ssh, "-----BEGIN SSH SIGNATURE-----").
14+ SIGNATURE_FORMAT_SSH = 1;
15+ // OpenPGP signature (gpg.format=openpgp, "-----BEGIN PGP SIGNATURE-----").
16+ SIGNATURE_FORMAT_GPG = 2;
17+ // X.509 (gpgsm) signature. Recognized, never verified.
18+ SIGNATURE_FORMAT_X509 = 3;
19+}
20+
21+// SignatureStatus is the outcome of checking the signature of a commit.
22+enum SignatureStatus {
23+ // The server did not check the commit (it could not be read).
24+ SIGNATURE_STATUS_UNSPECIFIED = 0;
25+ // The commit carries no signature.
26+ SIGNATURE_STATUS_UNSIGNED = 1;
27+ // Valid signature by a trusted key that is trusted for the committer email.
28+ SIGNATURE_STATUS_VERIFIED = 2;
29+ // Well-formed signature by a key the server does not trust.
30+ SIGNATURE_STATUS_UNKNOWN_KEY = 3;
31+ // The signature is malformed, does not match the commit, or uses a weak
32+ // hash (SHA-1 and older).
33+ SIGNATURE_STATUS_BAD_SIGNATURE = 4;
34+ // Valid signature by a trusted key that is not trusted for the committer
35+ // email.
36+ SIGNATURE_STATUS_EMAIL_MISMATCH = 5;
37+ // The OpenPGP key or signature had expired when the signature was made.
38+ SIGNATURE_STATUS_EXPIRED = 6;
39+ // The OpenPGP key is revoked.
40+ SIGNATURE_STATUS_REVOKED = 7;
41+ // The format is not verified by the server (X.509).
42+ SIGNATURE_STATUS_UNSUPPORTED = 8;
43+}
44+
45+// CommitVerification tells whether and how a commit is signed.
46+message CommitVerification {
47+ SignatureFormat format = 1;
48+ SignatureStatus status = 2;
49+ // Fingerprint of the signing key when the signature names one:
50+ // "SHA256:<base64>" for SSH, upper-case hex for OpenPGP (the 16-digit
51+ // issuer key id when the key is unknown).
52+ string fingerprint = 3;
53+ // KeyId is the id of the trusted key that matched; empty when none did.
54+ string key_id = 4;
55+ // KeyTitle is the title of the trusted key that matched.
56+ string key_title = 5;
57+}
58+
59+// TrustedKey is a public key whose signatures the server accepts for the
60+// listed committer emails.
61+message TrustedKey {
62+ // Id is the key fingerprint: "SHA256:<base64>" for SSH, the primary key
63+ // fingerprint in upper-case hex for OpenPGP.
64+ string id = 1;
65+ // Format is SIGNATURE_FORMAT_SSH or SIGNATURE_FORMAT_GPG.
66+ SignatureFormat format = 2;
67+ string title = 3;
68+ // PublicKey is "<type> <base64>" for SSH, the armored public key for
69+ // OpenPGP.
70+ string public_key = 4;
71+ // Emails are the committer emails the key may sign for, lower-case.
72+ repeated string emails = 5;
73+ google.protobuf.Timestamp created_at = 6;
74+ // Builtin is true for the server's own signing key
75+ // (ServerService.GetSigningKey): always trusted, cannot be removed.
76+ bool builtin = 7;
77+}
78+
79+// TrustedKeyService manages the keys commit signatures are checked against.
80+// Servers that implement it list "commit_signatures" in
81+// GetVersionResponse.features; their Commit messages then carry
82+// verification.
83+service TrustedKeyService {
84+ // ListTrustedKeys lists the keys, the builtin key first. Any authenticated
85+ // caller may.
86+ rpc ListTrustedKeys(ListTrustedKeysRequest) returns (ListTrustedKeysResponse);
87+ // AddTrustedKey adds a key. Owner only. invalid_argument for a key that
88+ // does not parse, a private key, no or too many emails; already_exists for
89+ // a key already listed; resource_exhausted beyond 100 keys.
90+ rpc AddTrustedKey(AddTrustedKeyRequest) returns (AddTrustedKeyResponse);
91+ // RemoveTrustedKey removes a key. Owner only. not_found for an unknown id;
92+ // failed_precondition for the builtin key.
93+ rpc RemoveTrustedKey(RemoveTrustedKeyRequest) returns (RemoveTrustedKeyResponse);
94+}
95+
96+// ListTrustedKeysRequest is empty.
97+message ListTrustedKeysRequest {}
98+
99+// ListTrustedKeysResponse lists every trusted key.
100+message ListTrustedKeysResponse {
101+ repeated TrustedKey keys = 1;
102+}
103+
104+// AddTrustedKeyRequest describes the key to trust.
105+message AddTrustedKeyRequest {
106+ // Format is SIGNATURE_FORMAT_SSH or SIGNATURE_FORMAT_GPG.
107+ SignatureFormat format = 1;
108+ // Title is a label, 1 to 100 bytes.
109+ string title = 2;
110+ // PublicKey is an authorized_keys line for SSH, or an armored public key
111+ // block for OpenPGP (at most 64 KiB).
112+ string public_key = 3;
113+ // Emails are the committer emails the key may sign for, 1 to 10.
114+ repeated string emails = 4;
115+}
116+
117+// AddTrustedKeyResponse returns the stored key.
118+message AddTrustedKeyResponse {
119+ TrustedKey key = 1;
120+}
121+
122+// RemoveTrustedKeyRequest names the key to remove.
123+message RemoveTrustedKeyRequest {
124+ string id = 1;
125+}
126+
127+// RemoveTrustedKeyResponse is empty.
128+message RemoveTrustedKeyResponse {}
M proto/ermite/types.proto +4 -0
@@ -4,6 +4,7 @@ syntax = "proto3";
44
55 package ermite;
66
7+import "ermite/signature.proto";
78 import "google/protobuf/timestamp.proto";
89
910 // Signature identifies who did something in Git and when: the author or the
@@ -34,6 +35,9 @@ message Commit {
3435 // Message is the full commit message, including the summary. Text that is
3536 // not valid UTF-8 is converted with the replacement character.
3637 string message = 6;
38+ // Verification tells whether and how the commit is signed. Unset on
39+ // servers without the "commit_signatures" feature.
40+ CommitVerification verification = 7;
3741 }
3842
3943 // MergeMode is how a merge request is merged. Both modes need the target to
@@ -4,6 +4,7 @@ syntax = "proto3";
4 4
5 package ermite;5 package ermite;
6 6
7+import "ermite/signature.proto";
7 import "google/protobuf/timestamp.proto";8 import "google/protobuf/timestamp.proto";
8 9
9 // Signature identifies who did something in Git and when: the author or the10 // Signature identifies who did something in Git and when: the author or the
@@ -34,6 +35,9 @@ message Commit {
34 // Message is the full commit message, including the summary. Text that is35 // Message is the full commit message, including the summary. Text that is
35 // not valid UTF-8 is converted with the replacement character.36 // not valid UTF-8 is converted with the replacement character.
36 string message = 6;37 string message = 6;
38+ // Verification tells whether and how the commit is signed. Unset on
39+ // servers without the "commit_signatures" feature.
40+ CommitVerification verification = 7;
37 }41 }
38 42
39 // MergeMode is how a merge request is merged. Both modes need the target to43 // MergeMode is how a merge request is merged. Both modes need the target to

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 18

↑↓ moveenter openesc close