@idpartners/client-attestation-sdk 0.2.0 → 1.0.1
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 +35 -4
- package/dist/attester.d.ts +93 -0
- package/dist/attester.js +139 -0
- package/dist/builders.d.ts +4 -8
- package/dist/builders.js +18 -10
- package/dist/evidence.d.ts +4 -19
- package/dist/evidence.js +38 -20
- package/dist/http.d.ts +1 -41
- package/dist/http.js +30 -5
- package/dist/index.d.ts +11 -3
- package/dist/index.js +8 -2
- package/dist/issuance.d.ts +22 -3
- package/dist/issuance.js +37 -30
- package/dist/logging.d.ts +12 -0
- package/dist/logging.js +77 -0
- package/dist/runtime.d.ts +1 -0
- package/dist/runtime.js +64 -0
- package/dist/signer.d.ts +2 -3
- package/dist/signer.js +9 -4
- package/dist/signing-key-pair.d.ts +8 -14
- package/dist/signing-key-pair.js +20 -5
- package/dist/svid-source.d.ts +15 -1
- package/dist/svid-source.js +20 -7
- package/dist/token-source.d.ts +43 -16
- package/dist/token-source.js +162 -75
- package/dist/validator/config.d.ts +5 -8
- package/dist/validator/config.js +22 -5
- package/dist/validator/index.d.ts +4 -3
- package/dist/validator/index.js +3 -3
- package/dist/validator/jwks.d.ts +0 -37
- package/dist/validator/jwks.js +17 -2
- package/dist/validator/resource.d.ts +1 -6
- package/dist/validator/resource.js +10 -1
- package/dist/validator/result.d.ts +0 -2
- package/dist/validator/result.js +2 -0
- package/dist/validator/strict.d.ts +1 -0
- package/dist/validator/strict.js +202 -0
- package/dist/validator/validator.d.ts +8 -0
- package/dist/validator/validator.js +120 -77
- package/package.json +9 -6
package/dist/http.js
CHANGED
|
@@ -12,11 +12,22 @@
|
|
|
12
12
|
* into a {@link TransportError}.
|
|
13
13
|
*/
|
|
14
14
|
import { TransportError } from "./errors.js";
|
|
15
|
+
import { utf8Decode, utf8Encode } from "./runtime.js";
|
|
16
|
+
/** @internal */
|
|
15
17
|
export const DEFAULT_MAX_BYTES = 1024 * 1024;
|
|
18
|
+
/** @internal */
|
|
16
19
|
export const DEFAULT_TIMEOUT_MS = 20_000;
|
|
17
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* At most this many characters of a response body are carried in an error.
|
|
22
|
+
*
|
|
23
|
+
* @internal
|
|
24
|
+
*/
|
|
18
25
|
export const ERROR_BODY_CHARS = 512;
|
|
19
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* The first {@link ERROR_BODY_CHARS} characters of a body, for an error message.
|
|
28
|
+
*
|
|
29
|
+
* @internal
|
|
30
|
+
*/
|
|
20
31
|
export function snippet(body) {
|
|
21
32
|
return body.length > ERROR_BODY_CHARS ? body.slice(0, ERROR_BODY_CHARS) : body;
|
|
22
33
|
}
|
|
@@ -31,6 +42,8 @@ function isAbort(err) {
|
|
|
31
42
|
* Make one HTTP request. Resolves with the status, headers and body text for any non-3xx response
|
|
32
43
|
* (non-2xx included); rejects with a {@link TransportError} for a network failure, timeout, redirect or
|
|
33
44
|
* oversized body.
|
|
45
|
+
*
|
|
46
|
+
* @internal
|
|
34
47
|
*/
|
|
35
48
|
export async function request(url, opts = {}) {
|
|
36
49
|
const method = opts.method ?? "GET";
|
|
@@ -98,7 +111,7 @@ async function readCapped(res, maxBytes, tooLarge) {
|
|
|
98
111
|
if (!res.body) {
|
|
99
112
|
// only reached by stand-in Response objects (tests); a real fetch always has a stream
|
|
100
113
|
const text = await res.text();
|
|
101
|
-
if (
|
|
114
|
+
if (utf8Encode(text).byteLength > maxBytes)
|
|
102
115
|
throw tooLarge();
|
|
103
116
|
return text;
|
|
104
117
|
}
|
|
@@ -116,12 +129,20 @@ async function readCapped(res, maxBytes, tooLarge) {
|
|
|
116
129
|
}
|
|
117
130
|
chunks.push(value);
|
|
118
131
|
}
|
|
119
|
-
|
|
132
|
+
const all = new Uint8Array(total);
|
|
133
|
+
let offset = 0;
|
|
134
|
+
for (const chunk of chunks) {
|
|
135
|
+
all.set(chunk, offset);
|
|
136
|
+
offset += chunk.byteLength;
|
|
137
|
+
}
|
|
138
|
+
return utf8Decode(all);
|
|
120
139
|
}
|
|
121
140
|
/**
|
|
122
141
|
* Parse `res.body` as a JSON object. Throws a {@link TransportError} for a non-2xx status (carrying the
|
|
123
142
|
* status and at most 512 characters of the body) and for a body that is not a JSON object. An empty body
|
|
124
143
|
* reads as `{}` when `allowEmpty` is set.
|
|
144
|
+
*
|
|
145
|
+
* @internal
|
|
125
146
|
*/
|
|
126
147
|
export function jsonOk(res, opts = {}) {
|
|
127
148
|
if (!res.ok) {
|
|
@@ -133,7 +154,11 @@ export function jsonOk(res, opts = {}) {
|
|
|
133
154
|
}
|
|
134
155
|
return parseJsonObject(res, opts);
|
|
135
156
|
}
|
|
136
|
-
/**
|
|
157
|
+
/**
|
|
158
|
+
* Parse `res.body` as a JSON object regardless of status; a {@link TransportError} if it is not one.
|
|
159
|
+
*
|
|
160
|
+
* @internal
|
|
161
|
+
*/
|
|
137
162
|
export function parseJsonObject(res, opts = {}) {
|
|
138
163
|
if (opts.allowEmpty && res.body === "")
|
|
139
164
|
return {};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Client-side builder SDK for OAuth Attestation-Based Client Authentication.
|
|
3
|
+
*
|
|
4
|
+
* Everything exported here is the package's API; docs/api.md says which parts are Stable and which are
|
|
5
|
+
* Experimental. Nothing else under dist/ can be imported: package.json "exports" allows only this entry
|
|
6
|
+
* point and "./validator".
|
|
3
7
|
*/
|
|
4
8
|
export { SigningKeyPair } from "./signing-key-pair.js";
|
|
5
9
|
export { ClientAttestationBuilder, DpopProofBuilder, PopBuilder, ATTESTATION_TYP, POP_TYP, DPOP_TYP, } from "./builders.js";
|
|
@@ -10,9 +14,13 @@ export { AttestationIssuanceClient, IssuedAttestation, IssuanceError, INSTANCE_P
|
|
|
10
14
|
export { StaticSvidSource, HttpSvidSource, SpireWorkloadApiSvidSource, } from "./svid-source.js";
|
|
11
15
|
export type { SvidSource } from "./svid-source.js";
|
|
12
16
|
export { EvidenceNotFound, FileTokenSvidSource, GoogleMetadataSvidSource, autoDetectEvidence, ENV_SVID, ENV_TOKEN_FILE, ENV_SPIFFE_SOCKET, DEFAULT_TOKEN_FILE, DEFAULT_SPIFFE_SOCKET, } from "./evidence.js";
|
|
13
|
-
export { ClientAttestation, ClientAttestationError, TransportError,
|
|
14
|
-
export type { ClientAttestationOptions } from "./token-source.js";
|
|
17
|
+
export { ClientAttestation, ClientAttestationError, TransportError, DEFAULT_EXPIRY_SKEW_SECONDS, } from "./token-source.js";
|
|
18
|
+
export type { AttesterDiscovery, ClientAttestationOptions } from "./token-source.js";
|
|
19
|
+
export { MintRequest, PingFederateProfile } from "./attester.js";
|
|
20
|
+
export type { AttesterMetadata, AttesterProfile, AttesterTransport, MintResult } from "./attester.js";
|
|
15
21
|
export type { TransportFailure } from "./errors.js";
|
|
16
|
-
export
|
|
22
|
+
export { NOOP_LOGGER } from "./logging.js";
|
|
23
|
+
export type { Logger, LogAttributes, LogLevel } from "./logging.js";
|
|
24
|
+
export declare const VERSION = "1.0.1";
|
|
17
25
|
/** draft-ietf-oauth-attestation-based-client-auth revision this SDK implements. */
|
|
18
26
|
export declare const DRAFT_REVISION = "10";
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Client-side builder SDK for OAuth Attestation-Based Client Authentication.
|
|
3
|
+
*
|
|
4
|
+
* Everything exported here is the package's API; docs/api.md says which parts are Stable and which are
|
|
5
|
+
* Experimental. Nothing else under dist/ can be imported: package.json "exports" allows only this entry
|
|
6
|
+
* point and "./validator".
|
|
3
7
|
*/
|
|
4
8
|
export { SigningKeyPair } from "./signing-key-pair.js";
|
|
5
9
|
export { ClientAttestationBuilder, DpopProofBuilder, PopBuilder, ATTESTATION_TYP, POP_TYP, DPOP_TYP, } from "./builders.js";
|
|
@@ -8,7 +12,9 @@ export { OpenBaoTransitSigner } from "./signer.js";
|
|
|
8
12
|
export { AttestationIssuanceClient, IssuedAttestation, IssuanceError, INSTANCE_PROOF_TYP, } from "./issuance.js";
|
|
9
13
|
export { StaticSvidSource, HttpSvidSource, SpireWorkloadApiSvidSource, } from "./svid-source.js";
|
|
10
14
|
export { EvidenceNotFound, FileTokenSvidSource, GoogleMetadataSvidSource, autoDetectEvidence, ENV_SVID, ENV_TOKEN_FILE, ENV_SPIFFE_SOCKET, DEFAULT_TOKEN_FILE, DEFAULT_SPIFFE_SOCKET, } from "./evidence.js";
|
|
11
|
-
export { ClientAttestation, ClientAttestationError, TransportError,
|
|
12
|
-
export
|
|
15
|
+
export { ClientAttestation, ClientAttestationError, TransportError, DEFAULT_EXPIRY_SKEW_SECONDS, } from "./token-source.js";
|
|
16
|
+
export { MintRequest, PingFederateProfile } from "./attester.js";
|
|
17
|
+
export { NOOP_LOGGER } from "./logging.js";
|
|
18
|
+
export const VERSION = "1.0.1";
|
|
13
19
|
/** draft-ietf-oauth-attestation-based-client-auth revision this SDK implements. */
|
|
14
20
|
export const DRAFT_REVISION = "10";
|
package/dist/issuance.d.ts
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client for the Client Attestation **issuance** endpoint (`POST /federation/attestation`).
|
|
3
|
+
*
|
|
4
|
+
* The inverse of {@link ClientAttestationBuilder}: instead of building an attestation locally with an
|
|
5
|
+
* attester key, a workload proves its SPIFFE identity (a JWT-SVID) and the Authorization Server
|
|
6
|
+
* (PingFederate) validates it and **mints** a Client Attestation bound to the workload's own instance
|
|
7
|
+
* key — bootstrapping a client credential from infra identity, no pre-shared secret.
|
|
8
|
+
*/
|
|
9
|
+
import { type AttesterProfile } from "./attester.js";
|
|
1
10
|
import { SigningKeyPair } from "./signing-key-pair.js";
|
|
2
11
|
import type { SvidSource } from "./svid-source.js";
|
|
3
|
-
/** The instance-key proof typ
|
|
4
|
-
export
|
|
12
|
+
/** The instance-key proof typ the PingFederate profile presents (server InstanceKeyProofValidator.TYP). */
|
|
13
|
+
export { INSTANCE_PROOF_TYP } from "./attester.js";
|
|
5
14
|
/**
|
|
6
15
|
* Thrown when the issuance endpoint rejects the request (non-2xx). Transport failures — unreachable,
|
|
7
16
|
* timeout, redirect, oversized or malformed response — are a {@link TransportError} instead.
|
|
@@ -25,17 +34,27 @@ export declare class IssuedAttestation {
|
|
|
25
34
|
*/
|
|
26
35
|
dpop(method: string, uri: string, challenge?: string): Promise<string>;
|
|
27
36
|
}
|
|
28
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* Bootstraps a Client Attestation for a workload from its SPIFFE JWT-SVID.
|
|
39
|
+
*
|
|
40
|
+
* `opts.profile` sets the mint protocol (default {@link PingFederateProfile}; see
|
|
41
|
+
* docs/attester-profiles.md). The endpoint is given directly, so the profile's discovery is not used.
|
|
42
|
+
*
|
|
43
|
+
* @experimental See docs/api.md: it calls a PingFederate demo issuance servlet (/federation/attestation)
|
|
44
|
+
* whose protocol is not standardised.
|
|
45
|
+
*/
|
|
29
46
|
export declare class AttestationIssuanceClient {
|
|
30
47
|
readonly endpoint: string;
|
|
31
48
|
readonly clientId: string;
|
|
32
49
|
readonly attesterIssuer: string;
|
|
33
50
|
private readonly source;
|
|
34
51
|
readonly instanceKey: SigningKeyPair;
|
|
52
|
+
readonly profile: AttesterProfile;
|
|
35
53
|
private constructor();
|
|
36
54
|
/** Generate a fresh ES256 instance key and return a ready client. */
|
|
37
55
|
static create(endpoint: string, clientId: string, attesterIssuer: string, source: SvidSource, opts?: {
|
|
38
56
|
instanceKey?: SigningKeyPair;
|
|
57
|
+
profile?: AttesterProfile;
|
|
39
58
|
}): Promise<AttestationIssuanceClient>;
|
|
40
59
|
/** Fetch the SVID, prove possession of the instance key, and exchange them for a minted attestation. */
|
|
41
60
|
bootstrap(authorizationDetails?: Array<Record<string, unknown>>, challenge?: string): Promise<IssuedAttestation>;
|
package/dist/issuance.js
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
* (PingFederate) validates it and **mints** a Client Attestation bound to the workload's own instance
|
|
7
7
|
* key — bootstrapping a client credential from infra identity, no pre-shared secret.
|
|
8
8
|
*/
|
|
9
|
-
import {
|
|
9
|
+
import { MintRequest, PingFederateProfile } from "./attester.js";
|
|
10
10
|
import { DpopProofBuilder, PopBuilder } from "./builders.js";
|
|
11
11
|
import { jsonOk, request } from "./http.js";
|
|
12
|
-
import { SigningKeyPair
|
|
13
|
-
/** The instance-key proof typ
|
|
14
|
-
export
|
|
12
|
+
import { SigningKeyPair } from "./signing-key-pair.js";
|
|
13
|
+
/** The instance-key proof typ the PingFederate profile presents (server InstanceKeyProofValidator.TYP). */
|
|
14
|
+
export { INSTANCE_PROOF_TYP } from "./attester.js";
|
|
15
15
|
/** Per-call timeout for the issuance endpoint. */
|
|
16
16
|
const ISSUANCE_TIMEOUT_MS = 20_000;
|
|
17
17
|
/**
|
|
@@ -58,53 +58,60 @@ export class IssuedAttestation {
|
|
|
58
58
|
return builder.build();
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
|
-
/**
|
|
61
|
+
/**
|
|
62
|
+
* Bootstraps a Client Attestation for a workload from its SPIFFE JWT-SVID.
|
|
63
|
+
*
|
|
64
|
+
* `opts.profile` sets the mint protocol (default {@link PingFederateProfile}; see
|
|
65
|
+
* docs/attester-profiles.md). The endpoint is given directly, so the profile's discovery is not used.
|
|
66
|
+
*
|
|
67
|
+
* @experimental See docs/api.md: it calls a PingFederate demo issuance servlet (/federation/attestation)
|
|
68
|
+
* whose protocol is not standardised.
|
|
69
|
+
*/
|
|
62
70
|
export class AttestationIssuanceClient {
|
|
63
71
|
endpoint;
|
|
64
72
|
clientId;
|
|
65
73
|
attesterIssuer;
|
|
66
74
|
source;
|
|
67
75
|
instanceKey;
|
|
68
|
-
|
|
76
|
+
profile;
|
|
77
|
+
constructor(endpoint, clientId, attesterIssuer, source, instanceKey, profile) {
|
|
69
78
|
this.endpoint = endpoint;
|
|
70
79
|
this.clientId = clientId;
|
|
71
80
|
this.attesterIssuer = attesterIssuer;
|
|
72
81
|
this.source = source;
|
|
73
82
|
this.instanceKey = instanceKey;
|
|
83
|
+
this.profile = profile;
|
|
74
84
|
}
|
|
75
85
|
/** Generate a fresh ES256 instance key and return a ready client. */
|
|
76
86
|
static async create(endpoint, clientId, attesterIssuer, source, opts) {
|
|
77
87
|
const key = opts?.instanceKey ?? (await SigningKeyPair.generate("ES256"));
|
|
78
|
-
|
|
88
|
+
const profile = opts?.profile ?? new PingFederateProfile();
|
|
89
|
+
return new AttestationIssuanceClient(endpoint.replace(/\/$/, ""), clientId, attesterIssuer, source, key, profile);
|
|
79
90
|
}
|
|
80
91
|
/** Fetch the SVID, prove possession of the instance key, and exchange them for a minted attestation. */
|
|
81
92
|
async bootstrap(authorizationDetails, challenge) {
|
|
82
93
|
const svid = await this.source.fetch(this.attesterIssuer);
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
body.authorization_details = authorizationDetails;
|
|
99
|
-
const res = await request(this.endpoint, {
|
|
100
|
-
method: "POST",
|
|
94
|
+
const minted = await this.profile.mint(issuanceTransport, { evidenceAudience: this.attesterIssuer, attestationEndpoint: this.endpoint }, new MintRequest({
|
|
95
|
+
instanceKey: this.instanceKey,
|
|
96
|
+
evidence: svid,
|
|
97
|
+
challenge: challenge || undefined,
|
|
98
|
+
clientId: this.clientId,
|
|
99
|
+
authorizationDetails,
|
|
100
|
+
}));
|
|
101
|
+
return new IssuedAttestation(minted.attestation, minted.expiresIn, this.instanceKey);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/** The issuance endpoint's transport: a non-2xx answer is an {@link IssuanceError}. */
|
|
105
|
+
const issuanceTransport = {
|
|
106
|
+
async requestJson(_step, method, url, body) {
|
|
107
|
+
const res = await request(url, {
|
|
108
|
+
method,
|
|
101
109
|
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
102
|
-
body: JSON.stringify(body),
|
|
110
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
103
111
|
timeoutMs: ISSUANCE_TIMEOUT_MS,
|
|
104
112
|
});
|
|
105
113
|
if (!res.ok)
|
|
106
114
|
throw new IssuanceError(res.status, res.body);
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
115
|
+
return jsonOk(res);
|
|
116
|
+
},
|
|
117
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Attributes carried with an event. */
|
|
2
|
+
export type LogAttributes = Record<string, unknown>;
|
|
3
|
+
/** Anything with these four methods: `console` fits, as do pino, winston and bunyan wrappers. */
|
|
4
|
+
export interface Logger {
|
|
5
|
+
debug(msg: string, attrs?: LogAttributes): void;
|
|
6
|
+
info(msg: string, attrs?: LogAttributes): void;
|
|
7
|
+
warn(msg: string, attrs?: LogAttributes): void;
|
|
8
|
+
error(msg: string, attrs?: LogAttributes): void;
|
|
9
|
+
}
|
|
10
|
+
export type LogLevel = "debug" | "info" | "warn" | "error";
|
|
11
|
+
/** The default: drops everything. */
|
|
12
|
+
export declare const NOOP_LOGGER: Logger;
|
package/dist/logging.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional logging. The SDK is silent unless a {@link Logger} is passed in (`logger` on
|
|
3
|
+
* `ClientAttestation` and on `AccessTokenValidator`). Event names and attribute keys are shared with the
|
|
4
|
+
* other ports; see docs/logging.md.
|
|
5
|
+
*
|
|
6
|
+
* Never logged: tokens, attestations, proofs, SVIDs, challenge values, key material, header values, or
|
|
7
|
+
* URLs with their query string. As a backstop, any string attribute containing `eyJ` (how a JWT's
|
|
8
|
+
* base64url-encoded header begins) is replaced with `<redacted:N bytes>` before it reaches the logger.
|
|
9
|
+
*/
|
|
10
|
+
import { TransportError } from "./errors.js";
|
|
11
|
+
import { utf8Encode } from "./runtime.js";
|
|
12
|
+
const noop = () => undefined;
|
|
13
|
+
/** The default: drops everything. */
|
|
14
|
+
export const NOOP_LOGGER = { debug: noop, info: noop, warn: noop, error: noop };
|
|
15
|
+
function guard(value) {
|
|
16
|
+
if (typeof value !== "string")
|
|
17
|
+
return value;
|
|
18
|
+
// A JWT's header and claims (any base64url-encoded JSON object) begin with "eyJ".
|
|
19
|
+
if (value.includes("eyJ")) {
|
|
20
|
+
return `<redacted:${utf8Encode(value).length} bytes>`;
|
|
21
|
+
}
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Emit one event: undefined attributes dropped, JWT-looking values redacted. Never throws.
|
|
26
|
+
*
|
|
27
|
+
* @internal
|
|
28
|
+
*/
|
|
29
|
+
export function emit(logger, level, event, attrs = {}) {
|
|
30
|
+
if (logger === NOOP_LOGGER)
|
|
31
|
+
return;
|
|
32
|
+
const clean = {};
|
|
33
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
34
|
+
if (value !== undefined && value !== null)
|
|
35
|
+
clean[key] = guard(value);
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
logger[level](event, clean);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// a broken logger must never break the flow
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* `host[:port]` and the path of a URL, never its query string or fragment.
|
|
46
|
+
*
|
|
47
|
+
* @internal
|
|
48
|
+
*/
|
|
49
|
+
export function urlParts(url) {
|
|
50
|
+
try {
|
|
51
|
+
const parsed = new URL(url);
|
|
52
|
+
return { host: parsed.host, path: parsed.pathname };
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return {};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* `host[:port]` of a URL.
|
|
60
|
+
*
|
|
61
|
+
* @internal
|
|
62
|
+
*/
|
|
63
|
+
export function hostOf(url) {
|
|
64
|
+
return urlParts(url).host;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The `reason` attribute for a failed outbound call.
|
|
68
|
+
*
|
|
69
|
+
* @internal
|
|
70
|
+
*/
|
|
71
|
+
export function failureReason(err) {
|
|
72
|
+
if (err instanceof TransportError)
|
|
73
|
+
return err.kind === "json" ? "malformed" : err.kind;
|
|
74
|
+
if (err instanceof SyntaxError)
|
|
75
|
+
return "malformed";
|
|
76
|
+
return "network";
|
|
77
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime-neutral helpers, so the package loads on edge runtimes (Cloudflare Workers, Vercel Edge,
|
|
3
|
+
* Deno Deploy) as well as Node. Nothing here, or anywhere else in `src`, imports a Node builtin
|
|
4
|
+
* statically: the few features that need one load it lazily through {@link importNodeBuiltin}.
|
|
5
|
+
* Not exported from the package index.
|
|
6
|
+
*/
|
|
7
|
+
import { ClientAttestationError } from "./errors.js";
|
|
8
|
+
const textEncoder = new TextEncoder();
|
|
9
|
+
const textDecoder = new TextDecoder();
|
|
10
|
+
/**
|
|
11
|
+
* UTF-8 bytes of a string.
|
|
12
|
+
*
|
|
13
|
+
* @internal
|
|
14
|
+
*/
|
|
15
|
+
export function utf8Encode(text) {
|
|
16
|
+
return textEncoder.encode(text);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* A string from UTF-8 bytes; malformed sequences become U+FFFD, as `Buffer#toString` does.
|
|
20
|
+
*
|
|
21
|
+
* @internal
|
|
22
|
+
*/
|
|
23
|
+
export function utf8Decode(bytes) {
|
|
24
|
+
return textDecoder.decode(bytes);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Standard (padded) base64 of some bytes.
|
|
28
|
+
*
|
|
29
|
+
* @internal
|
|
30
|
+
*/
|
|
31
|
+
export function base64Encode(bytes) {
|
|
32
|
+
let binary = "";
|
|
33
|
+
for (let i = 0; i < bytes.length; i += 0x8000) {
|
|
34
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
|
|
35
|
+
}
|
|
36
|
+
return btoa(binary);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* An environment variable, or `undefined` on a runtime with no `process.env` (most edge runtimes).
|
|
40
|
+
*
|
|
41
|
+
* @internal
|
|
42
|
+
*/
|
|
43
|
+
export function envVar(name) {
|
|
44
|
+
const proc = globalThis.process;
|
|
45
|
+
return proc?.env?.[name];
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Load a Node builtin (`node:<name>`) on first use. Throws a {@link ClientAttestationError} naming the
|
|
49
|
+
* feature when the runtime has no such module (an edge runtime without Node compatibility).
|
|
50
|
+
*
|
|
51
|
+
* The specifier is built at run time so bundlers for edge targets leave it alone instead of failing to
|
|
52
|
+
* resolve it.
|
|
53
|
+
*
|
|
54
|
+
* @internal
|
|
55
|
+
*/
|
|
56
|
+
export async function importNodeBuiltin(name, feature) {
|
|
57
|
+
const specifier = "node:" + name;
|
|
58
|
+
try {
|
|
59
|
+
return (await import(/* @vite-ignore */ /* webpackIgnore: true */ specifier));
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
throw new ClientAttestationError(`${feature} needs the Node.js module '${specifier}', which this runtime does not provide`, { cause: err });
|
|
63
|
+
}
|
|
64
|
+
}
|
package/dist/signer.d.ts
CHANGED
|
@@ -13,14 +13,13 @@ export interface JwsSigner {
|
|
|
13
13
|
publicJwk(): JWK;
|
|
14
14
|
sign(signingInput: Uint8Array): Promise<Uint8Array>;
|
|
15
15
|
}
|
|
16
|
-
/** Assemble a compact JWS whose signature is produced by an external signer (header carries `kid`; external
|
|
17
|
-
* signers hold issuing keys, referenced by id, never an embedded jwk). */
|
|
18
|
-
export declare function signExternal(claims: Record<string, unknown>, signer: JwsSigner, typ: string): Promise<string>;
|
|
19
16
|
/**
|
|
20
17
|
* A {@link JwsSigner} backed by an OpenBao / HashiCorp Vault transit engine: the attestation is signed
|
|
21
18
|
* inside the vault (`POST /v1/transit/sign/<key>` with `marshaling_algorithm=jws`, which returns the
|
|
22
19
|
* fixed-width `r||s`) and the private key never leaves it. `create` reads the key metadata to pin the
|
|
23
20
|
* latest version, derive the public JWK and compute its RFC 7638 `kid`. Fail-closed (vault errors throw).
|
|
21
|
+
*
|
|
22
|
+
* @experimental See docs/api.md: a client for a third-party API that authenticates with a static token only; its constructor will change when other auth methods are added.
|
|
24
23
|
*/
|
|
25
24
|
export declare class OpenBaoTransitSigner implements JwsSigner {
|
|
26
25
|
#private;
|
package/dist/signer.js
CHANGED
|
@@ -4,13 +4,16 @@
|
|
|
4
4
|
* {@link SigningKeyPair} covers the local-key case; {@link OpenBaoTransitSigner} signs inside an
|
|
5
5
|
* OpenBao / HashiCorp Vault transit engine so the attester's private key never leaves it.
|
|
6
6
|
*/
|
|
7
|
-
import { calculateJwkThumbprint, exportJWK, importSPKI } from "jose";
|
|
7
|
+
import { base64url, calculateJwkThumbprint, exportJWK, importSPKI } from "jose";
|
|
8
8
|
import { TransportError } from "./errors.js";
|
|
9
|
+
import { base64Encode } from "./runtime.js";
|
|
9
10
|
import { parseJsonObject, request as httpRequest, snippet } from "./http.js";
|
|
10
11
|
/** Per-call timeout for the vault. */
|
|
11
12
|
const OPENBAO_TIMEOUT_MS = 5_000;
|
|
12
|
-
const b64url = (bytes) =>
|
|
13
|
+
const b64url = (bytes) => base64url.encode(bytes);
|
|
13
14
|
/** Assemble a compact JWS whose signature is produced by an external signer (header carries `kid`; external
|
|
15
|
+
*
|
|
16
|
+
* @internal
|
|
14
17
|
* signers hold issuing keys, referenced by id, never an embedded jwk). */
|
|
15
18
|
export async function signExternal(claims, signer, typ) {
|
|
16
19
|
const enc = new TextEncoder();
|
|
@@ -26,6 +29,8 @@ const ALG_HASH = { ES256: "sha2-256", ES384: "sha2-384", ES512: "sha2-512" };
|
|
|
26
29
|
* inside the vault (`POST /v1/transit/sign/<key>` with `marshaling_algorithm=jws`, which returns the
|
|
27
30
|
* fixed-width `r||s`) and the private key never leaves it. `create` reads the key metadata to pin the
|
|
28
31
|
* latest version, derive the public JWK and compute its RFC 7638 `kid`. Fail-closed (vault errors throw).
|
|
32
|
+
*
|
|
33
|
+
* @experimental See docs/api.md: a client for a third-party API that authenticates with a static token only; its constructor will change when other auth methods are added.
|
|
29
34
|
*/
|
|
30
35
|
export class OpenBaoTransitSigner {
|
|
31
36
|
algorithm;
|
|
@@ -67,7 +72,7 @@ export class OpenBaoTransitSigner {
|
|
|
67
72
|
}
|
|
68
73
|
async sign(signingInput) {
|
|
69
74
|
const body = JSON.stringify({
|
|
70
|
-
input:
|
|
75
|
+
input: base64Encode(signingInput),
|
|
71
76
|
marshaling_algorithm: "jws",
|
|
72
77
|
hash_algorithm: this.#hash,
|
|
73
78
|
key_version: this.#keyVersion,
|
|
@@ -78,7 +83,7 @@ export class OpenBaoTransitSigner {
|
|
|
78
83
|
throw new Error("transit sign response carried no signature");
|
|
79
84
|
}
|
|
80
85
|
const raw = signature.slice(signature.lastIndexOf(":") + 1); // envelope: vault:v<n>:<base64url(r||s)>
|
|
81
|
-
return
|
|
86
|
+
return base64url.decode(raw);
|
|
82
87
|
}
|
|
83
88
|
}
|
|
84
89
|
async function request(method, base, token, path, body) {
|
|
@@ -19,11 +19,17 @@ export declare class SigningKeyPair {
|
|
|
19
19
|
readonly keyId: string;
|
|
20
20
|
private constructor();
|
|
21
21
|
/**
|
|
22
|
-
* Generate a fresh
|
|
22
|
+
* Generate a fresh key pair for the given JWS algorithm: `ES256` / `ES384` / `ES512`
|
|
23
23
|
* (EC P-256 / P-384 / P-521), or `RS256` / `RS384` / `RS512` / `PS256` / `PS384` / `PS512`
|
|
24
24
|
* (RSA 2048).
|
|
25
|
+
*
|
|
26
|
+
* The private key is not extractable unless `options.extractable` is true: nothing in the SDK needs
|
|
27
|
+
* to export it, so by default it can't leak through `exportJWK`. Pass `{ extractable: true }` only to
|
|
28
|
+
* persist the key yourself (then restore it with {@link SigningKeyPair.fromJwk}).
|
|
25
29
|
*/
|
|
26
|
-
static generate(algorithm: string
|
|
30
|
+
static generate(algorithm: string, options?: {
|
|
31
|
+
extractable?: boolean;
|
|
32
|
+
}): Promise<SigningKeyPair>;
|
|
27
33
|
/**
|
|
28
34
|
* Wrap an existing JWK (which must contain the private component `d`) for the given algorithm.
|
|
29
35
|
*/
|
|
@@ -36,15 +42,3 @@ export declare class SigningKeyPair {
|
|
|
36
42
|
*/
|
|
37
43
|
publicJwk(): JWK;
|
|
38
44
|
}
|
|
39
|
-
/** The JWS algorithms {@link SigningKeyPair} can generate and sign with. */
|
|
40
|
-
export declare const SUPPORTED_ALGORITHMS: readonly string[];
|
|
41
|
-
/** RFC 7638 JWK thumbprint (SHA-256, base64url) over the required members only. */
|
|
42
|
-
export declare function thumbprint(publicJwk: JWK): Promise<string>;
|
|
43
|
-
/**
|
|
44
|
-
* Sign claims into a compact JWS with an explicit `typ` header, keyed by `key`.
|
|
45
|
-
*
|
|
46
|
-
* When `embedJwk` is set the public key travels in a `jwk` header (as DPoP requires); otherwise
|
|
47
|
-
* the key's thumbprint `kid` is set.
|
|
48
|
-
*/
|
|
49
|
-
export declare function signCompact(claims: Record<string, unknown>, key: SigningKeyPair, typ: string, embedJwk: boolean): Promise<string>;
|
|
50
|
-
export declare function requireText(value: string | null | undefined, field: string): string;
|
package/dist/signing-key-pair.js
CHANGED
|
@@ -26,14 +26,18 @@ export class SigningKeyPair {
|
|
|
26
26
|
this.keyId = keyId;
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
29
|
-
* Generate a fresh
|
|
29
|
+
* Generate a fresh key pair for the given JWS algorithm: `ES256` / `ES384` / `ES512`
|
|
30
30
|
* (EC P-256 / P-384 / P-521), or `RS256` / `RS384` / `RS512` / `PS256` / `PS384` / `PS512`
|
|
31
31
|
* (RSA 2048).
|
|
32
|
+
*
|
|
33
|
+
* The private key is not extractable unless `options.extractable` is true: nothing in the SDK needs
|
|
34
|
+
* to export it, so by default it can't leak through `exportJWK`. Pass `{ extractable: true }` only to
|
|
35
|
+
* persist the key yourself (then restore it with {@link SigningKeyPair.fromJwk}).
|
|
32
36
|
*/
|
|
33
|
-
static async generate(algorithm) {
|
|
37
|
+
static async generate(algorithm, options = {}) {
|
|
34
38
|
checkAlgorithm(algorithm);
|
|
35
39
|
const { privateKey, publicKey } = await generateKeyPair(algorithm, {
|
|
36
|
-
extractable:
|
|
40
|
+
extractable: options.extractable ?? false,
|
|
37
41
|
...(algorithm.startsWith("ES") ? {} : { modulusLength: RSA_MODULUS_BITS }),
|
|
38
42
|
});
|
|
39
43
|
const publicJwk = publicMembers(await exportJWK(publicKey));
|
|
@@ -66,7 +70,11 @@ export class SigningKeyPair {
|
|
|
66
70
|
return { ...this.#publicJwk };
|
|
67
71
|
}
|
|
68
72
|
}
|
|
69
|
-
/**
|
|
73
|
+
/**
|
|
74
|
+
* The JWS algorithms {@link SigningKeyPair} can generate and sign with.
|
|
75
|
+
*
|
|
76
|
+
* @internal
|
|
77
|
+
*/
|
|
70
78
|
export const SUPPORTED_ALGORITHMS = [
|
|
71
79
|
"ES256", "ES384", "ES512",
|
|
72
80
|
"RS256", "RS384", "RS512",
|
|
@@ -85,7 +93,11 @@ function publicMembers(jwk) {
|
|
|
85
93
|
? { kty: jwk.kty, n: jwk.n, e: jwk.e }
|
|
86
94
|
: { kty: jwk.kty, crv: jwk.crv, x: jwk.x, y: jwk.y };
|
|
87
95
|
}
|
|
88
|
-
/**
|
|
96
|
+
/**
|
|
97
|
+
* RFC 7638 JWK thumbprint (SHA-256, base64url) over the required members only.
|
|
98
|
+
*
|
|
99
|
+
* @internal
|
|
100
|
+
*/
|
|
89
101
|
export async function thumbprint(publicJwk) {
|
|
90
102
|
return calculateJwkThumbprint(publicJwk, "sha256");
|
|
91
103
|
}
|
|
@@ -94,6 +106,8 @@ export async function thumbprint(publicJwk) {
|
|
|
94
106
|
*
|
|
95
107
|
* When `embedJwk` is set the public key travels in a `jwk` header (as DPoP requires); otherwise
|
|
96
108
|
* the key's thumbprint `kid` is set.
|
|
109
|
+
*
|
|
110
|
+
* @internal
|
|
97
111
|
*/
|
|
98
112
|
export async function signCompact(claims, key, typ, embedJwk) {
|
|
99
113
|
const header = { alg: key.algorithm, typ };
|
|
@@ -107,6 +121,7 @@ export async function signCompact(claims, key, typ, embedJwk) {
|
|
|
107
121
|
}
|
|
108
122
|
return new SignJWT(claims).setProtectedHeader(header).sign(key.privateKey);
|
|
109
123
|
}
|
|
124
|
+
/** @internal */
|
|
110
125
|
export function requireText(value, field) {
|
|
111
126
|
if (value == null || String(value).trim() === "") {
|
|
112
127
|
throw new Error(`${field} is required`);
|
package/dist/svid-source.d.ts
CHANGED
|
@@ -8,7 +8,15 @@ export declare class StaticSvidSource implements SvidSource {
|
|
|
8
8
|
constructor(svid: string);
|
|
9
9
|
fetch(_audience: string): Promise<string>;
|
|
10
10
|
}
|
|
11
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* GET a URL returning `{"svid":"<jwt>"}` — e.g. the demo harness `/dev/svid` endpoint.
|
|
13
|
+
*
|
|
14
|
+
* For development and testing only. It trusts whatever the URL returns and the endpoint has no caller
|
|
15
|
+
* authentication, so anyone who can reach it can obtain the workload's SVID. In production use a SPIRE,
|
|
16
|
+
* file or cloud source.
|
|
17
|
+
*
|
|
18
|
+
* @experimental See docs/api.md: a development and test source with no caller authentication.
|
|
19
|
+
*/
|
|
12
20
|
export declare class HttpSvidSource implements SvidSource {
|
|
13
21
|
private readonly url;
|
|
14
22
|
constructor(url: string);
|
|
@@ -18,6 +26,12 @@ export declare class HttpSvidSource implements SvidSource {
|
|
|
18
26
|
* The default source: fetch a JWT-SVID from a real SPIRE agent's Workload API via the `spire-agent`
|
|
19
27
|
* CLI. Swap for a native Workload API client by implementing {@link SvidSource} if you prefer no
|
|
20
28
|
* subprocess.
|
|
29
|
+
*
|
|
30
|
+
* Needs Node.js (it spawns a process): `node:child_process` is loaded on the first `fetch`, which throws
|
|
31
|
+
* a `ClientAttestationError` on a runtime without it.
|
|
32
|
+
*
|
|
33
|
+
* @experimental See docs/api.md: it shells out to the spire-agent CLI, and its constructor will change
|
|
34
|
+
* when it moves to the Workload API socket.
|
|
21
35
|
*/
|
|
22
36
|
export declare class SpireWorkloadApiSvidSource implements SvidSource {
|
|
23
37
|
private readonly socketPath;
|