Skip to content

Commit fb393e2

Browse files
committed
Implement Identity-based Transparency Logging
This feature introduces a new log entry format and dedicated server backend to support Identity-based Transparency Logging as specified in c2sp.org/identity-transparency (currently at C2SP/C2SP#244). This change supports the new entry type, with only Ed25519 signatures as credentials. This will be proceeded by support for OIDC tokens as credentials, and ML-DSA signatures as credentials. Architectural Design & Changes: - **Protobuf Specifications**: Added `identity.proto` defining `IdentityRequestV001`, and implemented the identity type in `pkg/types/identity/identity.go` - **New Server Binary**: Leveraged the existing POSIX backend to create a new, isolated server binary (`cmd/rekor-server/identity-posix`). This ensures the identity log operates completely independently from the standard HashedRekord logs while reusing the existing storage interfaces. - **Infrastructure**: Updated `.goreleaser.yaml` and CI pipelines (`build_container.yml`, `test.yml`) to properly compile, test, and package the new `rekor-server-identity-posix` binaries and containers. Partially implements #827, with the remaining PRs to support ML-DSA and an additional GCP backend for PGI deployment. Coding assisted with Gemini, reviewed entirely by me. Signed-off-by: Hayden <8418760+Hayden-IO@users.noreply.github.com>
1 parent 030fe61 commit fb393e2

25 files changed

Lines changed: 1744 additions & 97 deletions

.github/workflows/build_container.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ jobs:
4848

4949
strategy:
5050
matrix:
51-
storage: [ 'gcp', 'posix', 'aws', 'gcpcloudsql' ]
51+
storage: [ 'gcp', 'posix', 'aws', 'gcpcloudsql', 'identity-posix' ]
5252

5353
steps:
5454
- name: Checkout repository

.github/workflows/test.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ jobs:
4747
runs-on: ubuntu-latest
4848
strategy:
4949
matrix:
50-
storage: [ 'gcp', 'posix', 'aws', 'gcpcloudsql' ]
50+
storage: [ 'gcp', 'posix', 'aws', 'gcpcloudsql', 'identity-posix' ]
5151
steps:
5252
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
5353
with:
@@ -115,7 +115,8 @@ jobs:
115115
storage: [ {name: 'GCP', compose: 'compose.yml', test: 'TestGCPSpanner'},
116116
{name: 'POSIX', compose: 'posix-compose.yml', test: 'TestPOSIX'},
117117
{name: 'AWS', compose: 'aws-compose.yml', test: 'TestAWS'},
118-
{name: 'GCP Cloud SQL', compose: 'cloudsql-compose.yml', test: 'TestGCPCloudSQL'} ]
118+
{name: 'GCP Cloud SQL', compose: 'cloudsql-compose.yml', test: 'TestGCPCloudSQL'},
119+
{name: 'Identity POSIX', compose: 'identity-posix-compose.yml', test: 'TestIdentityPOSIX'} ]
119120
steps:
120121
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
121122
with:

.goreleaser.yaml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,23 @@ builds:
6161
- CGO_ENABLED=0
6262
ldflags:
6363
- "{{ .Env.LDFLAGS }}"
64+
- id: rekor-server-identity-posix
65+
binary: rekor-server-identity-posix-{{ .Os }}-{{ .Arch }}
66+
main: ./cmd/rekor-server/identity-posix
67+
no_unique_dist_dir: true
68+
goos:
69+
- linux
70+
- darwin
71+
goarch:
72+
- amd64
73+
- arm64
74+
flags:
75+
- -trimpath
76+
mod_timestamp: '{{ .CommitTimestamp }}'
77+
env:
78+
- CGO_ENABLED=0
79+
ldflags:
80+
- "{{ .Env.LDFLAGS }}"
6481
- id: rekor-server-aws
6582
binary: rekor-server-aws-{{ .Os }}-{{ .Arch }}
6683
main: ./cmd/rekor-server/aws

Makefile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
.PHONY: all test clean lint gosec ko-local tools ldflags
1717

18-
all: protos rekor-server-gcp rekor-server-posix rekor-server-aws rekor-server-gcpcloudsql
18+
all: protos rekor-server-gcp rekor-server-posix rekor-server-aws rekor-server-gcpcloudsql rekor-server-identity-posix
1919

2020
GIT_VERSION ?= $(shell git describe --tags --always --dirty)
2121
GIT_HASH ?= $(shell git rev-parse HEAD)
@@ -81,6 +81,9 @@ rekor-server-gcp: $(SRC) $(PROTO_SRC)
8181
rekor-server-posix: $(SRC) $(PROTO_SRC)
8282
CGO_ENABLED=0 go build -trimpath -ldflags "$(SERVER_LDFLAGS)" -o rekor-server-posix ./cmd/rekor-server/posix
8383

84+
rekor-server-identity-posix: $(SRC) $(PROTO_SRC)
85+
CGO_ENABLED=0 go build -trimpath -ldflags "$(SERVER_LDFLAGS)" -o rekor-server-identity-posix ./cmd/rekor-server/identity-posix
86+
8487
rekor-server-aws: $(SRC) $(PROTO_SRC)
8588
CGO_ENABLED=0 go build -trimpath -ldflags "$(SERVER_LDFLAGS)" -o rekor-server-aws ./cmd/rekor-server/aws
8689

api/proto/rekor/v2/identity.proto

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Copyright 2026 The Sigstore Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
syntax = "proto3";
16+
package dev.sigstore.rekor.v2;
17+
18+
import "google/api/field_behavior.proto";
19+
20+
option go_package = "github.com/sigstore/rekor-tiles/v2/pkg/generated/protobuf";
21+
22+
option java_package = "dev.sigstore.proto.rekor.v2";
23+
option java_multiple_files = true;
24+
option java_outer_classname = "RekorV2Identity";
25+
option ruby_package = "Sigstore::Rekor::V2";
26+
27+
message PublicKeyCredential {
28+
// The public key used to verify the signature.
29+
// MUST be an Ed25519 public key (SPKI DER-encoded).
30+
bytes public_key = 1 [(google.api.field_behavior) = REQUIRED];
31+
32+
// The signature computed over the specification identifier, checksum, and context strings,
33+
// as defined by the c2sp.org/identity-transparency specification.
34+
bytes signature = 2 [(google.api.field_behavior) = REQUIRED];
35+
}
36+
37+
// A request to add an identity-based transparency log entry (v0.0.1)
38+
message IdentityRequestV001 {
39+
oneof credential {
40+
PublicKeyCredential public_key = 1;
41+
}
42+
43+
// Must be a SHA-256 digest of data.
44+
bytes message = 3 [(google.api.field_behavior) = REQUIRED];
45+
46+
// Optional mapping of hashed context keys to hashed values.
47+
// Limited to 20 context strings.
48+
repeated ContextEntry context = 4;
49+
}
50+
51+
message ContextEntry {
52+
// Must be a SHA-256 digest of the key.
53+
bytes key = 1 [(google.api.field_behavior) = REQUIRED];
54+
// Must be a SHA-256 digest of the value.
55+
bytes value = 2 [(google.api.field_behavior) = REQUIRED];
56+
}
57+
58+

api/proto/rekor/v2/rekor_service.proto

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import "sigstore_rekor.proto";
2222
import "protoc-gen-openapiv2/options/annotations.proto";
2323

2424
import "rekor/v2/entry.proto";
25+
import "rekor/v2/identity.proto";
2526

2627
option go_package = "github.com/sigstore/rekor-tiles/v2/pkg/generated/protobuf";
2728

@@ -67,6 +68,14 @@ service Rekor {
6768
};
6869
}
6970

71+
// Create an identity entry in the log
72+
rpc CreateIdentityEntry (IdentityRequestV001) returns (google.api.HttpBody) {
73+
option (google.api.http) = {
74+
post: "/api/v2/log/identities"
75+
body: "*"
76+
};
77+
}
78+
7079
// Get a tile from the log
7180
rpc GetTile (TileRequest) returns (google.api.HttpBody) {
7281
option (google.api.http) = {
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
//
2+
// Copyright 2026 The Sigstore Authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package app
17+
18+
import (
19+
"log/slog"
20+
"os"
21+
22+
"github.com/spf13/cobra"
23+
)
24+
25+
var rootCmd = &cobra.Command{
26+
Use: "rekor-server",
27+
Short: "Rekor signature transparency log server (Identity)",
28+
Long: `Rekor provides signature transparency for Sigstore's software
29+
signing infrastructure. This instance provides an identity-based log leveraging a POSIX-compliant filesystem`,
30+
}
31+
32+
// Execute adds all child commands to the root command and sets flags appropriately.
33+
// This is called by main.main(). It only needs to happen once to the rootCmd.
34+
func Execute() {
35+
if err := rootCmd.Execute(); err != nil {
36+
slog.Error("failed to execute root command", "error", err)
37+
os.Exit(1)
38+
}
39+
}
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
//
2+
// Copyright 2026 The Sigstore Authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package app
17+
18+
import (
19+
"context"
20+
"crypto/x509"
21+
"encoding/base64"
22+
"log/slog"
23+
"os"
24+
25+
"k8s.io/klog/v2"
26+
27+
"github.com/spf13/cobra"
28+
"github.com/spf13/viper"
29+
"sigs.k8s.io/release-utils/version"
30+
31+
"github.com/sigstore/rekor-tiles/v2/internal/algorithmregistry"
32+
"github.com/sigstore/rekor-tiles/v2/internal/cli"
33+
"github.com/sigstore/rekor-tiles/v2/internal/server"
34+
"github.com/sigstore/rekor-tiles/v2/internal/signerverifier"
35+
"github.com/sigstore/rekor-tiles/v2/internal/tessera"
36+
posixDriver "github.com/sigstore/rekor-tiles/v2/internal/tessera/posix"
37+
"github.com/sigstore/rekor-tiles/v2/pkg/note"
38+
"github.com/sigstore/sigstore/pkg/signature/options"
39+
)
40+
41+
var serveCmd = &cobra.Command{
42+
Use: "serve",
43+
Short: "start the Rekor server",
44+
Long: "start the Rekor server",
45+
Run: func(cmd *cobra.Command, _ []string) {
46+
ctx := cmd.Context()
47+
48+
logLevel := slog.LevelInfo
49+
if err := logLevel.UnmarshalText([]byte(viper.GetString("log-level"))); err != nil {
50+
slog.Error("invalid log-level specified; must be one of 'debug', 'info', 'error', or 'warn'")
51+
os.Exit(1)
52+
}
53+
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: logLevel})))
54+
55+
// tessera uses klog so pipe all klog messages to be written through slog
56+
klog.SetSlogLogger(slog.Default())
57+
58+
slog.Info("starting rekor-server", "version", version.GetVersionInfo())
59+
60+
if viper.GetString("signer-filepath") == "" {
61+
slog.Error("--signer-filepath must be set")
62+
os.Exit(1)
63+
}
64+
signer, err := signerverifier.NewFileSignerVerifier(viper.GetString("signer-filepath"), viper.GetString("signer-password"))
65+
if err != nil {
66+
slog.Error("failed to initialize signer", "error", err)
67+
os.Exit(1)
68+
}
69+
pubkey, err := signer.PublicKey()
70+
if err != nil {
71+
slog.Error("failed to get public key from signing key", "error", err)
72+
os.Exit(1)
73+
}
74+
der, err := x509.MarshalPKIXPublicKey(pubkey)
75+
if err != nil {
76+
slog.Error("failed to marshal public key to DER", "error", err)
77+
os.Exit(1)
78+
}
79+
slog.Info("Loaded signing key", "pubkey in base64 DER", base64.StdEncoding.EncodeToString(der))
80+
81+
appendOptions, err := tessera.NewAppendOptions(ctx, viper.GetString("hostname"), signer)
82+
if err != nil {
83+
slog.Error("failed to initialize append options", "error", err)
84+
os.Exit(1)
85+
}
86+
// Compute log ID for TransparencyLogEntry, to be used by clients to look up
87+
// the correct instance in a trust root. Log ID is equivalent to the non-truncated
88+
// hash of the public key and origin per the signed-note C2SP spec.
89+
pubKey, err := signer.PublicKey(options.WithContext(ctx))
90+
if err != nil {
91+
slog.Error("failed to get public key", "error", err)
92+
os.Exit(1)
93+
}
94+
_, logID, err := note.KeyHash(viper.GetString("hostname"), pubKey)
95+
if err != nil {
96+
slog.Error("failed to get log ID", "error", err)
97+
os.Exit(1)
98+
}
99+
100+
readOnly := viper.GetBool("read-only")
101+
var tesseraStorage tessera.Storage
102+
shutdownFn := func(_ context.Context) error { return nil }
103+
// if in read-only mode, don't start the appender, because we don't want new checkpoints being published.
104+
if !readOnly {
105+
driverConfig := posixDriver.DriverConfiguration{
106+
StorageDir: viper.GetString("storage-dir"),
107+
PersistentAntispam: viper.GetBool("persistent-antispam"),
108+
ASMaxBatchSize: viper.GetUint("antispam-max-batch-size"),
109+
ASPushbackThreshold: viper.GetUint("antispam-pushback-threshold"),
110+
}
111+
tesseraDriver, persistentAntispam, err := posixDriver.NewDriver(ctx, driverConfig)
112+
if err != nil {
113+
slog.Error("failed to initialize driver", "error", err)
114+
os.Exit(1)
115+
}
116+
appendOptions = tessera.WithLifecycleOptions(appendOptions, viper.GetUint("batch-max-size"), viper.GetDuration("batch-max-age"), viper.GetDuration("checkpoint-interval"), viper.GetUint("pushback-max-outstanding"))
117+
appendOptions = tessera.WithAntispamOptions(appendOptions, persistentAntispam)
118+
if wpf := viper.GetString("witness-policy-path"); wpf != "" {
119+
f, err := os.ReadFile(wpf)
120+
if err != nil {
121+
slog.Error("failed to read witness policy file", "file", wpf, "error", err)
122+
os.Exit(1)
123+
}
124+
appendOptions, err = tessera.WithWitnessing(appendOptions, f)
125+
if err != nil {
126+
slog.Error("failed to initialize witnessing", "error", err)
127+
os.Exit(1)
128+
}
129+
}
130+
tesseraStorage, shutdownFn, err = tessera.NewStorage(ctx, viper.GetString("hostname"), tesseraDriver, appendOptions)
131+
if err != nil {
132+
slog.Error("failed to initialize tessera storage", "error", err)
133+
os.Exit(1)
134+
}
135+
}
136+
algorithmRegistry, err := algorithmregistry.AlgorithmRegistry(viper.GetStringSlice("client-signing-algorithms"))
137+
if err != nil {
138+
slog.Error("failed to get algorithm registry", "error", err)
139+
os.Exit(1)
140+
}
141+
142+
rekorServer := server.NewIdentityServer(tesseraStorage, readOnly, algorithmRegistry, logID)
143+
144+
server.Serve(
145+
ctx,
146+
server.NewHTTPConfig(
147+
server.WithHTTPPort(viper.GetInt("http-port")),
148+
server.WithHTTPHost(viper.GetString("http-address")),
149+
server.WithHTTPTimeout(viper.GetDuration("server-timeout")),
150+
server.WithHTTPMaxRequestBodySize(viper.GetInt("max-request-body-size")),
151+
server.WithHTTPMetricsPort(viper.GetInt("http-metrics-port")),
152+
server.WithHTTPTLSCredentials(viper.GetString("http-tls-cert-file"), viper.GetString("http-tls-key-file")),
153+
server.WithGRPCTLSCredentials(viper.GetString("grpc-tls-cert-file")),
154+
),
155+
server.NewGRPCConfig(
156+
server.WithGRPCPort(viper.GetInt("grpc-port")),
157+
server.WithGRPCHost(viper.GetString("grpc-address")),
158+
server.WithGRPCTimeout(viper.GetDuration("server-timeout")),
159+
server.WithGRPCMaxMessageSize(viper.GetInt("max-request-body-size")),
160+
server.WithGRPCLogLevel(logLevel, viper.GetBool("request-response-logging")),
161+
server.WithTLSCredentials(viper.GetString("grpc-tls-cert-file"), viper.GetString("grpc-tls-key-file")),
162+
),
163+
viper.GetDuration("tlog-timeout"),
164+
rekorServer,
165+
shutdownFn,
166+
)
167+
},
168+
}
169+
170+
func init() {
171+
if err := cli.Initialize(serveCmd); err != nil {
172+
slog.Error(err.Error())
173+
os.Exit(1)
174+
}
175+
176+
// POSIX configs
177+
serveCmd.Flags().String("storage-dir", "", "directory for tile and checkpoint storage for a POSIX log")
178+
179+
// checkpoint signing configs
180+
serveCmd.Flags().String("signer-filepath", "", "path to the signing key")
181+
serveCmd.Flags().String("signer-password", "", "password to decrypt the signing key")
182+
183+
if err := viper.BindPFlags(serveCmd.Flags()); err != nil {
184+
slog.Error(err.Error())
185+
os.Exit(1)
186+
}
187+
rootCmd.AddCommand(serveCmd)
188+
}

0 commit comments

Comments
 (0)