@enactprotocol/trust 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -0
- package/package.json +32 -0
- package/src/hash.ts +120 -0
- package/src/index.ts +28 -0
- package/src/keys.ts +157 -0
- package/src/sigstore/attestation.ts +501 -0
- package/src/sigstore/cosign.ts +564 -0
- package/src/sigstore/index.ts +139 -0
- package/src/sigstore/oauth/client.ts +89 -0
- package/src/sigstore/oauth/index.ts +95 -0
- package/src/sigstore/oauth/server.ts +163 -0
- package/src/sigstore/policy.ts +450 -0
- package/src/sigstore/signing.ts +569 -0
- package/src/sigstore/types.ts +613 -0
- package/src/sigstore/verification.ts +355 -0
- package/src/types.ts +80 -0
- package/tests/hash.test.ts +180 -0
- package/tests/index.test.ts +8 -0
- package/tests/keys.test.ts +147 -0
- package/tests/sigstore/attestation.test.ts +369 -0
- package/tests/sigstore/policy.test.ts +260 -0
- package/tests/sigstore/signing.test.ts +220 -0
- package/tsconfig.json +11 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth Client
|
|
3
|
+
*
|
|
4
|
+
* Wrapper around openid-client for PKCE-based OAuth flow with Sigstore.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { type BaseClient, Issuer, generators } from "openid-client";
|
|
8
|
+
|
|
9
|
+
interface OAuthClientOptions {
|
|
10
|
+
issuer: string;
|
|
11
|
+
redirectURL: string;
|
|
12
|
+
clientID: string;
|
|
13
|
+
clientSecret: string | undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Initialize an OAuth client by discovering the issuer's configuration
|
|
18
|
+
*/
|
|
19
|
+
export async function initializeOAuthClient(options: OAuthClientOptions): Promise<OAuthClient> {
|
|
20
|
+
const issuer = await Issuer.discover(options.issuer);
|
|
21
|
+
|
|
22
|
+
const client = new issuer.Client(
|
|
23
|
+
options.clientSecret
|
|
24
|
+
? {
|
|
25
|
+
client_id: options.clientID,
|
|
26
|
+
client_secret: options.clientSecret,
|
|
27
|
+
token_endpoint_auth_method: "client_secret_basic" as const,
|
|
28
|
+
}
|
|
29
|
+
: {
|
|
30
|
+
client_id: options.clientID,
|
|
31
|
+
token_endpoint_auth_method: "none" as const,
|
|
32
|
+
}
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
return new OAuthClient(client, options.redirectURL);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* OAuthClient wraps an openid-client Client instance to maintain
|
|
40
|
+
* state for the PKCE authorization flow.
|
|
41
|
+
*/
|
|
42
|
+
export class OAuthClient {
|
|
43
|
+
private client: BaseClient;
|
|
44
|
+
private redirectURL: string;
|
|
45
|
+
private verifier: string;
|
|
46
|
+
private nonce: string;
|
|
47
|
+
private state: string;
|
|
48
|
+
|
|
49
|
+
constructor(client: BaseClient, redirectURL: string) {
|
|
50
|
+
this.client = client;
|
|
51
|
+
this.redirectURL = redirectURL;
|
|
52
|
+
this.verifier = generators.codeVerifier(32);
|
|
53
|
+
this.nonce = generators.nonce(32);
|
|
54
|
+
this.state = generators.state(16);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get the authorization URL to redirect the user to
|
|
59
|
+
*/
|
|
60
|
+
get authorizationUrl(): string {
|
|
61
|
+
return this.client.authorizationUrl({
|
|
62
|
+
scope: "openid email",
|
|
63
|
+
redirect_uri: this.redirectURL,
|
|
64
|
+
code_challenge: generators.codeChallenge(this.verifier),
|
|
65
|
+
code_challenge_method: "S256",
|
|
66
|
+
state: this.state,
|
|
67
|
+
nonce: this.nonce,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Exchange the callback URL for an ID token
|
|
73
|
+
*/
|
|
74
|
+
public async getIDToken(callbackURL: string): Promise<string> {
|
|
75
|
+
const params = this.client.callbackParams(callbackURL);
|
|
76
|
+
const tokenSet = await this.client.callback(this.redirectURL, params, {
|
|
77
|
+
response_type: "code",
|
|
78
|
+
code_verifier: this.verifier,
|
|
79
|
+
state: this.state,
|
|
80
|
+
nonce: this.nonce,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
if (!tokenSet.id_token) {
|
|
84
|
+
throw new Error("No ID token received from OAuth provider");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return tokenSet.id_token;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth Identity Provider
|
|
3
|
+
*
|
|
4
|
+
* Provides interactive OIDC authentication for keyless signing.
|
|
5
|
+
* Opens a browser for the user to authenticate with their identity provider
|
|
6
|
+
* (GitHub, Google, Microsoft) and returns an OIDC token that can be used
|
|
7
|
+
* with Fulcio to obtain a signing certificate.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import open from "open";
|
|
11
|
+
import { initializeOAuthClient } from "./client";
|
|
12
|
+
import { CallbackServer } from "./server";
|
|
13
|
+
|
|
14
|
+
/** Default Sigstore OAuth issuer */
|
|
15
|
+
export const SIGSTORE_OAUTH_ISSUER = "https://oauth2.sigstore.dev/auth";
|
|
16
|
+
|
|
17
|
+
/** Default Sigstore OAuth client ID */
|
|
18
|
+
export const SIGSTORE_CLIENT_ID = "sigstore";
|
|
19
|
+
|
|
20
|
+
export interface OAuthIdentityProviderOptions {
|
|
21
|
+
/** OIDC issuer URL (default: Sigstore public instance) */
|
|
22
|
+
issuer?: string;
|
|
23
|
+
/** OAuth client ID (default: "sigstore") */
|
|
24
|
+
clientID?: string;
|
|
25
|
+
/** OAuth client secret (optional, not needed for public clients) */
|
|
26
|
+
clientSecret?: string;
|
|
27
|
+
/** Redirect URL (optional, auto-generated if not provided) */
|
|
28
|
+
redirectURL?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* IdentityProvider interface - matches sigstore's expected interface
|
|
33
|
+
*/
|
|
34
|
+
export interface IdentityProvider {
|
|
35
|
+
getToken: () => Promise<string>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* OAuthIdentityProvider implements interactive browser-based OAuth flow
|
|
40
|
+
* to obtain an OIDC token for keyless signing.
|
|
41
|
+
*/
|
|
42
|
+
export class OAuthIdentityProvider implements IdentityProvider {
|
|
43
|
+
private server: CallbackServer;
|
|
44
|
+
private issuer: string;
|
|
45
|
+
private clientID: string;
|
|
46
|
+
private clientSecret: string | undefined;
|
|
47
|
+
|
|
48
|
+
constructor(options: OAuthIdentityProviderOptions = {}) {
|
|
49
|
+
this.issuer = options.issuer ?? SIGSTORE_OAUTH_ISSUER;
|
|
50
|
+
this.clientID = options.clientID ?? SIGSTORE_CLIENT_ID;
|
|
51
|
+
this.clientSecret = options.clientSecret;
|
|
52
|
+
|
|
53
|
+
let serverOpts: { hostname: string; port: number };
|
|
54
|
+
if (options.redirectURL) {
|
|
55
|
+
const url = new URL(options.redirectURL);
|
|
56
|
+
serverOpts = { hostname: url.hostname, port: Number(url.port) };
|
|
57
|
+
} else {
|
|
58
|
+
// Use random port on localhost
|
|
59
|
+
serverOpts = { hostname: "localhost", port: 0 };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
this.server = new CallbackServer(serverOpts);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get an OIDC token by performing interactive OAuth flow.
|
|
67
|
+
* Opens a browser for the user to authenticate.
|
|
68
|
+
*/
|
|
69
|
+
public async getToken(): Promise<string> {
|
|
70
|
+
// Start server to receive OAuth callback
|
|
71
|
+
const serverURL = await this.server.start();
|
|
72
|
+
|
|
73
|
+
// Initialize OAuth client with discovered configuration
|
|
74
|
+
const client = await initializeOAuthClient({
|
|
75
|
+
issuer: this.issuer,
|
|
76
|
+
redirectURL: serverURL,
|
|
77
|
+
clientID: this.clientID,
|
|
78
|
+
clientSecret: this.clientSecret,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Open browser to OAuth login page
|
|
82
|
+
await open(client.authorizationUrl);
|
|
83
|
+
|
|
84
|
+
if (!this.server.callback) {
|
|
85
|
+
throw new Error("callback server not started");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Wait for callback and exchange auth code for ID token
|
|
89
|
+
return this.server.callback.then((callbackURL) => client.getIDToken(callbackURL));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Re-export for convenience
|
|
94
|
+
export { CallbackServer } from "./server";
|
|
95
|
+
export { OAuthClient, initializeOAuthClient } from "./client";
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth Callback Server
|
|
3
|
+
*
|
|
4
|
+
* A simple HTTP server that receives the OAuth redirect callback
|
|
5
|
+
* after the user authenticates in their browser.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import http from "node:http";
|
|
9
|
+
import type { Socket } from "node:net";
|
|
10
|
+
|
|
11
|
+
interface CallbackServerOptions {
|
|
12
|
+
port: number;
|
|
13
|
+
hostname: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* CallbackServer is a simple HTTP server which receives the OAuth
|
|
18
|
+
* redirect from the OAuth provider after the user signs-in. It will shutdown
|
|
19
|
+
* once the callback is received and the callback promise will resolve with
|
|
20
|
+
* the URL of the incoming request.
|
|
21
|
+
*/
|
|
22
|
+
export class CallbackServer {
|
|
23
|
+
private server: http.Server;
|
|
24
|
+
private sockets: Set<Socket>;
|
|
25
|
+
private port: number;
|
|
26
|
+
private hostname: string;
|
|
27
|
+
|
|
28
|
+
public callback: Promise<string> | undefined;
|
|
29
|
+
|
|
30
|
+
constructor(options: CallbackServerOptions) {
|
|
31
|
+
this.server = http.createServer();
|
|
32
|
+
this.sockets = new Set<Socket>();
|
|
33
|
+
this.port = options.port;
|
|
34
|
+
this.hostname = options.hostname;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async start(): Promise<string> {
|
|
38
|
+
await new Promise<void>((resolve) => {
|
|
39
|
+
this.server.listen(this.port, this.hostname, resolve);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Keep track of connections so we can force a shutdown
|
|
43
|
+
this.server.on("connection", (socket) => {
|
|
44
|
+
this.sockets.add(socket);
|
|
45
|
+
socket.on("close", () => {
|
|
46
|
+
this.sockets.delete(socket);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// The callback will resolve with the incoming request URL
|
|
51
|
+
this.callback = new Promise<string>((resolve) => {
|
|
52
|
+
this.server.on("request", ({ url }, res) => {
|
|
53
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
54
|
+
res.end(AUTH_SUCCESS_HTML);
|
|
55
|
+
|
|
56
|
+
// Shutdown the server and resolve the callback promise
|
|
57
|
+
this.shutdown().then(() => resolve(url!));
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Calculate and return the URL which can be used to reach the server
|
|
62
|
+
return this.serverURL(this.server);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
public async shutdown(): Promise<void> {
|
|
66
|
+
// Destroy all sockets and close the server
|
|
67
|
+
return new Promise<void>((resolve) => {
|
|
68
|
+
for (const socket of this.sockets) {
|
|
69
|
+
socket.destroy();
|
|
70
|
+
this.sockets.delete(socket);
|
|
71
|
+
}
|
|
72
|
+
this.server.close(() => resolve());
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private serverURL(server: http.Server): string {
|
|
77
|
+
const address = server.address();
|
|
78
|
+
if (address === null) {
|
|
79
|
+
throw new Error("invalid server config: address is null");
|
|
80
|
+
}
|
|
81
|
+
if (typeof address === "string") {
|
|
82
|
+
throw new Error("invalid server config: address is a string");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return `http://${this.hostname}:${address.port}`;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Success HTML page shown after authentication
|
|
90
|
+
const AUTH_SUCCESS_HTML = `
|
|
91
|
+
<!DOCTYPE html>
|
|
92
|
+
<html>
|
|
93
|
+
<head>
|
|
94
|
+
<title>Enact - Authentication Successful</title>
|
|
95
|
+
<style>
|
|
96
|
+
:root { font-family: system-ui, -apple-system, sans-serif; }
|
|
97
|
+
body {
|
|
98
|
+
display: flex;
|
|
99
|
+
justify-content: center;
|
|
100
|
+
align-items: center;
|
|
101
|
+
min-height: 100vh;
|
|
102
|
+
margin: 0;
|
|
103
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
104
|
+
}
|
|
105
|
+
.container {
|
|
106
|
+
background: white;
|
|
107
|
+
padding: 3rem;
|
|
108
|
+
border-radius: 1rem;
|
|
109
|
+
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
|
|
110
|
+
text-align: center;
|
|
111
|
+
max-width: 400px;
|
|
112
|
+
}
|
|
113
|
+
.checkmark {
|
|
114
|
+
width: 80px;
|
|
115
|
+
height: 80px;
|
|
116
|
+
margin: 0 auto 1.5rem;
|
|
117
|
+
background: #10b981;
|
|
118
|
+
border-radius: 50%;
|
|
119
|
+
display: flex;
|
|
120
|
+
align-items: center;
|
|
121
|
+
justify-content: center;
|
|
122
|
+
}
|
|
123
|
+
.checkmark svg {
|
|
124
|
+
width: 40px;
|
|
125
|
+
height: 40px;
|
|
126
|
+
stroke: white;
|
|
127
|
+
stroke-width: 3;
|
|
128
|
+
fill: none;
|
|
129
|
+
}
|
|
130
|
+
h1 {
|
|
131
|
+
color: #1f2937;
|
|
132
|
+
margin: 0 0 0.5rem;
|
|
133
|
+
font-size: 1.5rem;
|
|
134
|
+
}
|
|
135
|
+
p {
|
|
136
|
+
color: #6b7280;
|
|
137
|
+
margin: 0;
|
|
138
|
+
font-size: 1rem;
|
|
139
|
+
}
|
|
140
|
+
.brand {
|
|
141
|
+
margin-top: 2rem;
|
|
142
|
+
color: #9ca3af;
|
|
143
|
+
font-size: 0.875rem;
|
|
144
|
+
}
|
|
145
|
+
.brand strong {
|
|
146
|
+
color: #667eea;
|
|
147
|
+
}
|
|
148
|
+
</style>
|
|
149
|
+
</head>
|
|
150
|
+
<body>
|
|
151
|
+
<div class="container">
|
|
152
|
+
<div class="checkmark">
|
|
153
|
+
<svg viewBox="0 0 24 24">
|
|
154
|
+
<polyline points="20 6 9 17 4 12"></polyline>
|
|
155
|
+
</svg>
|
|
156
|
+
</div>
|
|
157
|
+
<h1>Authentication Successful!</h1>
|
|
158
|
+
<p>You may now close this window and return to your terminal.</p>
|
|
159
|
+
<p class="brand">Signed with <strong>Sigstore</strong> via <strong>Enact</strong></p>
|
|
160
|
+
</div>
|
|
161
|
+
</body>
|
|
162
|
+
</html>
|
|
163
|
+
`;
|