mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Add more scheme
This commit is contained in:
436
backend/src/ee/services/pki-acme/pki-acme-errors.ts
Normal file
436
backend/src/ee/services/pki-acme/pki-acme-errors.ts
Normal file
@@ -0,0 +1,436 @@
|
||||
/**
|
||||
* ACME Error Classes based on RFC 8555 Section 6.2
|
||||
* https://datatracker.ietf.org/doc/html/rfc8555#section-6.2
|
||||
*/
|
||||
|
||||
export interface IAcmeError {
|
||||
type: string;
|
||||
detail: string;
|
||||
status: number;
|
||||
subproblems?: Array<{ type: string; detail: string; identifier?: { type: string; value: string } }>;
|
||||
}
|
||||
|
||||
export class AcmeError extends Error implements IAcmeError {
|
||||
type: string;
|
||||
|
||||
detail: string;
|
||||
|
||||
status: number;
|
||||
|
||||
subproblems?: Array<{ type: string; detail: string; identifier?: { type: string; value: string } }>;
|
||||
|
||||
error?: unknown;
|
||||
|
||||
constructor({
|
||||
type,
|
||||
detail,
|
||||
status,
|
||||
subproblems,
|
||||
error,
|
||||
message
|
||||
}: {
|
||||
type: string;
|
||||
detail: string;
|
||||
status: number;
|
||||
subproblems?: Array<{ type: string; detail: string; identifier?: { type: string; value: string } }>;
|
||||
error?: unknown;
|
||||
message?: string;
|
||||
}) {
|
||||
super(message || detail);
|
||||
this.type = type;
|
||||
this.detail = detail;
|
||||
this.status = status;
|
||||
this.subproblems = subproblems;
|
||||
this.error = error;
|
||||
this.name = "AcmeError";
|
||||
}
|
||||
|
||||
toAcmeResponse(): IAcmeError {
|
||||
return {
|
||||
type: this.type,
|
||||
detail: this.detail,
|
||||
status: this.status,
|
||||
subproblems: this.subproblems
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* malformed - The request message was malformed (RFC 8555 Section 6.7.1)
|
||||
*/
|
||||
export class AcmeMalformedError extends AcmeError {
|
||||
constructor({
|
||||
detail = "The request message was malformed",
|
||||
error,
|
||||
message
|
||||
}: {
|
||||
detail?: string;
|
||||
error?: unknown;
|
||||
message?: string;
|
||||
} = {}) {
|
||||
super({
|
||||
type: "malformed",
|
||||
detail,
|
||||
status: 400,
|
||||
error,
|
||||
message
|
||||
});
|
||||
this.name = "AcmeMalformedError";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* unauthorized - The client lacks sufficient authorization (RFC 8555 Section 6.7.2)
|
||||
*/
|
||||
export class AcmeUnauthorizedError extends AcmeError {
|
||||
constructor({
|
||||
detail = "The client lacks sufficient authorization",
|
||||
error,
|
||||
message
|
||||
}: {
|
||||
detail?: string;
|
||||
error?: unknown;
|
||||
message?: string;
|
||||
} = {}) {
|
||||
super({
|
||||
type: "unauthorized",
|
||||
detail,
|
||||
status: 403,
|
||||
error,
|
||||
message
|
||||
});
|
||||
this.name = "AcmeUnauthorizedError";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* accountDoesNotExist - The request specified an account that does not exist
|
||||
* (RFC 8555 Section 6.7.3)
|
||||
*/
|
||||
export class AcmeAccountDoesNotExistError extends AcmeError {
|
||||
constructor({
|
||||
detail = "The request specified an account that does not exist",
|
||||
error,
|
||||
message
|
||||
}: {
|
||||
detail?: string;
|
||||
error?: unknown;
|
||||
message?: string;
|
||||
} = {}) {
|
||||
super({
|
||||
type: "accountDoesNotExist",
|
||||
detail,
|
||||
status: 400,
|
||||
error,
|
||||
message
|
||||
});
|
||||
this.name = "AcmeAccountDoesNotExistError";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* badNonce - The client sent an unacceptable anti-replay nonce (RFC 8555 Section 6.7.4)
|
||||
*/
|
||||
export class AcmeBadNonceError extends AcmeError {
|
||||
constructor({
|
||||
detail = "The client sent an unacceptable anti-replay nonce",
|
||||
error,
|
||||
message
|
||||
}: {
|
||||
detail?: string;
|
||||
error?: unknown;
|
||||
message?: string;
|
||||
} = {}) {
|
||||
super({
|
||||
type: "badNonce",
|
||||
detail,
|
||||
status: 400,
|
||||
error,
|
||||
message
|
||||
});
|
||||
this.name = "AcmeBadNonceError";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* badSignature - The JWS signature is invalid (RFC 8555 Section 6.7.5)
|
||||
*/
|
||||
export class AcmeBadSignatureError extends AcmeError {
|
||||
constructor({
|
||||
detail = "The JWS signature is invalid",
|
||||
error,
|
||||
message
|
||||
}: {
|
||||
detail?: string;
|
||||
error?: unknown;
|
||||
message?: string;
|
||||
} = {}) {
|
||||
super({
|
||||
type: "badSignature",
|
||||
detail,
|
||||
status: 401,
|
||||
error,
|
||||
message
|
||||
});
|
||||
this.name = "AcmeBadSignatureError";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* badPublicKey - The public key is not acceptable (RFC 8555 Section 6.7.6)
|
||||
*/
|
||||
export class AcmeBadPublicKeyError extends AcmeError {
|
||||
constructor({
|
||||
detail = "The public key is not acceptable",
|
||||
error,
|
||||
message
|
||||
}: {
|
||||
detail?: string;
|
||||
error?: unknown;
|
||||
message?: string;
|
||||
} = {}) {
|
||||
super({
|
||||
type: "badPublicKey",
|
||||
detail,
|
||||
status: 400,
|
||||
error,
|
||||
message
|
||||
});
|
||||
this.name = "AcmeBadPublicKeyError";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* badCSR - The CSR is unacceptable (RFC 8555 Section 6.7.7)
|
||||
*/
|
||||
export class AcmeBadCsrError extends AcmeError {
|
||||
constructor({
|
||||
detail = "The CSR is unacceptable",
|
||||
error,
|
||||
message
|
||||
}: {
|
||||
detail?: string;
|
||||
error?: unknown;
|
||||
message?: string;
|
||||
} = {}) {
|
||||
super({
|
||||
type: "badCSR",
|
||||
detail,
|
||||
status: 400,
|
||||
error,
|
||||
message
|
||||
});
|
||||
this.name = "AcmeBadCsrError";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* badRevocationReason - The revocation reason provided is not allowed
|
||||
* (RFC 8555 Section 6.7.8)
|
||||
*/
|
||||
export class AcmeBadRevocationReasonError extends AcmeError {
|
||||
constructor({
|
||||
detail = "The revocation reason provided is not allowed",
|
||||
error,
|
||||
message
|
||||
}: {
|
||||
detail?: string;
|
||||
error?: unknown;
|
||||
message?: string;
|
||||
} = {}) {
|
||||
super({
|
||||
type: "badRevocationReason",
|
||||
detail,
|
||||
status: 400,
|
||||
error,
|
||||
message
|
||||
});
|
||||
this.name = "AcmeBadRevocationReasonError";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* rateLimited - The client has exceeded a rate limit (RFC 8555 Section 6.7.9)
|
||||
*/
|
||||
export class AcmeRateLimitedError extends AcmeError {
|
||||
constructor({
|
||||
detail = "The client has exceeded a rate limit",
|
||||
error,
|
||||
message
|
||||
}: {
|
||||
detail?: string;
|
||||
error?: unknown;
|
||||
message?: string;
|
||||
} = {}) {
|
||||
super({
|
||||
type: "rateLimited",
|
||||
detail,
|
||||
status: 429,
|
||||
error,
|
||||
message
|
||||
});
|
||||
this.name = "AcmeRateLimitedError";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* rejectedIdentifier - The server will not issue certificates for the identifier
|
||||
* (RFC 8555 Section 6.7.10)
|
||||
*/
|
||||
export class AcmeRejectedIdentifierError extends AcmeError {
|
||||
constructor({
|
||||
detail = "The server will not issue certificates for the identifier",
|
||||
subproblems,
|
||||
error,
|
||||
message
|
||||
}: {
|
||||
detail?: string;
|
||||
subproblems?: Array<{ type: string; detail: string; identifier?: { type: string; value: string } }>;
|
||||
error?: unknown;
|
||||
message?: string;
|
||||
} = {}) {
|
||||
super({
|
||||
type: "rejectedIdentifier",
|
||||
detail,
|
||||
status: 400,
|
||||
subproblems,
|
||||
error,
|
||||
message
|
||||
});
|
||||
this.name = "AcmeRejectedIdentifierError";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* serverInternal - An internal error occurred (RFC 8555 Section 6.7.11)
|
||||
*/
|
||||
export class AcmeServerInternalError extends AcmeError {
|
||||
constructor({
|
||||
detail = "An internal error occurred",
|
||||
error,
|
||||
message
|
||||
}: {
|
||||
detail?: string;
|
||||
error?: unknown;
|
||||
message?: string;
|
||||
} = {}) {
|
||||
super({
|
||||
type: "serverInternal",
|
||||
detail,
|
||||
status: 500,
|
||||
error,
|
||||
message
|
||||
});
|
||||
this.name = "AcmeServerInternalError";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* serviceUnavailable - The service is unavailable (RFC 8555 Section 6.7.12)
|
||||
*/
|
||||
export class AcmeServiceUnavailableError extends AcmeError {
|
||||
constructor({
|
||||
detail = "The service is unavailable",
|
||||
error,
|
||||
message
|
||||
}: {
|
||||
detail?: string;
|
||||
error?: unknown;
|
||||
message?: string;
|
||||
} = {}) {
|
||||
super({
|
||||
type: "serviceUnavailable",
|
||||
detail,
|
||||
status: 503,
|
||||
error,
|
||||
message
|
||||
});
|
||||
this.name = "AcmeServiceUnavailableError";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* unsupportedContact - A contact URL is of an unsupported type (RFC 8555 Section 6.7.13)
|
||||
*/
|
||||
export class AcmeUnsupportedContactError extends AcmeError {
|
||||
constructor({
|
||||
detail = "A contact URL is of an unsupported type",
|
||||
error,
|
||||
message
|
||||
}: {
|
||||
detail?: string;
|
||||
error?: unknown;
|
||||
message?: string;
|
||||
} = {}) {
|
||||
super({
|
||||
type: "unsupportedContact",
|
||||
detail,
|
||||
status: 400,
|
||||
error,
|
||||
message
|
||||
});
|
||||
this.name = "AcmeUnsupportedContactError";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* unsupportedIdentifier - An identifier is of an unsupported type
|
||||
* (RFC 8555 Section 6.7.14)
|
||||
*/
|
||||
export class AcmeUnsupportedIdentifierError extends AcmeError {
|
||||
constructor({
|
||||
detail = "An identifier is of an unsupported type",
|
||||
error,
|
||||
message
|
||||
}: {
|
||||
detail?: string;
|
||||
error?: unknown;
|
||||
message?: string;
|
||||
} = {}) {
|
||||
super({
|
||||
type: "unsupportedIdentifier",
|
||||
detail,
|
||||
status: 400,
|
||||
error,
|
||||
message
|
||||
});
|
||||
this.name = "AcmeUnsupportedIdentifierError";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* userActionRequired - Visit the "instance" URL and take actions specified there
|
||||
* (RFC 8555 Section 6.7.15)
|
||||
*/
|
||||
export class AcmeUserActionRequiredError extends AcmeError {
|
||||
instance?: string;
|
||||
|
||||
constructor({
|
||||
detail = "Visit the instance URL and take actions specified there",
|
||||
instance,
|
||||
error,
|
||||
message
|
||||
}: {
|
||||
detail?: string;
|
||||
instance?: string;
|
||||
error?: unknown;
|
||||
message?: string;
|
||||
} = {}) {
|
||||
super({
|
||||
type: "userActionRequired",
|
||||
detail,
|
||||
status: 403,
|
||||
error,
|
||||
message
|
||||
});
|
||||
this.instance = instance;
|
||||
this.name = "AcmeUserActionRequiredError";
|
||||
}
|
||||
|
||||
toAcmeResponse(): IAcmeError & { instance?: string } {
|
||||
return {
|
||||
...super.toAcmeResponse(),
|
||||
instance: this.instance
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,25 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const ProtectedHeaderSchema = z.object({
|
||||
alg: z.string(),
|
||||
nonce: z.string(),
|
||||
url: z.string(),
|
||||
kid: z.string().optional(),
|
||||
jwk: z.record(z.string(), z.string()).optional()
|
||||
});
|
||||
|
||||
// Raw JWS payload schema before parsing and verification
|
||||
export const RawJwsPayloadSchema = z.object({
|
||||
protected: z.string(),
|
||||
payload: z.string(),
|
||||
signature: z.string()
|
||||
});
|
||||
|
||||
export const JwsPayloadSchema = z.object({
|
||||
protectedHeader: ProtectedHeaderSchema,
|
||||
payload: z.any()
|
||||
});
|
||||
|
||||
// Directory endpoint
|
||||
export const GetAcmeDirectorySchema = z.object({
|
||||
params: z.object({
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { NotFoundError } from "@app/lib/errors";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
|
||||
import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal";
|
||||
|
||||
import { AcmeMalformedError, AcmeBadPublicKeyError } from "./pki-acme-errors";
|
||||
|
||||
import {
|
||||
EnrollmentType,
|
||||
TCertificateProfileWithConfigs
|
||||
@@ -19,18 +21,20 @@ import {
|
||||
TGetAcmeAuthorizationResponse,
|
||||
TGetAcmeDirectoryResponse,
|
||||
TGetAcmeOrderResponse,
|
||||
TRawJwsPayload,
|
||||
TListAcmeOrdersResponse,
|
||||
TPkiAcmeServiceFactory,
|
||||
TRespondToAcmeChallengeResponse
|
||||
TRespondToAcmeChallengeResponse,
|
||||
TJwsPayload,
|
||||
TProtectedHeader
|
||||
} from "./pki-acme-types";
|
||||
import { flattenedVerify, importJWK, JWK, JWSHeaderParameters } from "jose";
|
||||
|
||||
type TPkiAcmeServiceFactoryDep = {
|
||||
certificateProfileDAL: Pick<TCertificateProfileDALFactory, "findById">;
|
||||
};
|
||||
|
||||
export const pkiAcmeServiceFactory = ({ certificateProfileDAL }: TPkiAcmeServiceFactoryDep): TPkiAcmeServiceFactory => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
const validateAcmeProfile = async (profileId: string): Promise<TCertificateProfileWithConfigs> => {
|
||||
const profile = await certificateProfileDAL.findById(profileId);
|
||||
if (!profile) {
|
||||
@@ -43,18 +47,35 @@ export const pkiAcmeServiceFactory = ({ certificateProfileDAL }: TPkiAcmeService
|
||||
};
|
||||
|
||||
const buildUrl = (path: string): string => {
|
||||
const appCfg = getConfig();
|
||||
const baseUrl = appCfg.SITE_URL ?? "";
|
||||
return `${baseUrl}${path}`;
|
||||
};
|
||||
|
||||
const validateCreateAcmeAccountJwsPayload = async (rawPayload: TRawJwsPayload): Promise<TJwsPayload> => {
|
||||
const { payload, protectedHeader } = await flattenedVerify(
|
||||
rawPayload,
|
||||
async (protectedHeader: JWSHeaderParameters | undefined) => {
|
||||
if (protectedHeader === undefined) {
|
||||
throw new AcmeMalformedError({ detail: "Protected header is required" });
|
||||
}
|
||||
if (protectedHeader.jwk === undefined) {
|
||||
throw new AcmeBadPublicKeyError({ detail: "JWK is required in the protected header" });
|
||||
}
|
||||
// For the create account request, the JWK is provided in the protected header.
|
||||
// Let use it to verify the signature.
|
||||
const imported = await importJWK(protectedHeader.jwk as JWK, protectedHeader.alg);
|
||||
return imported;
|
||||
}
|
||||
);
|
||||
const decoder = new TextDecoder();
|
||||
const parsedPayload = JSON.parse(decoder.decode(payload)) as TCreateAcmeAccountPayload;
|
||||
// TODO: also consume the nonce here
|
||||
return { payload: parsedPayload, protectedHeader: protectedHeader as TProtectedHeader };
|
||||
};
|
||||
|
||||
const getAcmeDirectory = async (profileId: string): Promise<TGetAcmeDirectoryResponse> => {
|
||||
// FIXME: Implement ACME directory endpoint
|
||||
// Validate profile exists and is for ACME enrollment
|
||||
const profile = await validateAcmeProfile(profileId);
|
||||
|
||||
// FIXME: Validate profile is configured for ACME enrollment
|
||||
|
||||
// Return absolute URLs using SITE_URL
|
||||
await validateAcmeProfile(profileId);
|
||||
return {
|
||||
newNonce: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/new-nonce`),
|
||||
newAccount: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/new-account`),
|
||||
@@ -71,7 +92,7 @@ export const pkiAcmeServiceFactory = ({ certificateProfileDAL }: TPkiAcmeService
|
||||
|
||||
const createAcmeAccount = async (
|
||||
profileId: string,
|
||||
body: TCreateAcmeAccountPayload
|
||||
payload: TCreateAcmeAccountPayload
|
||||
): Promise<TCreateAcmeAccountResponse> => {
|
||||
const profile = await validateAcmeProfile(profileId);
|
||||
// FIXME: Implement ACME new account registration
|
||||
@@ -88,7 +109,7 @@ export const pkiAcmeServiceFactory = ({ certificateProfileDAL }: TPkiAcmeService
|
||||
|
||||
const createAcmeOrder = async (
|
||||
profileId: string,
|
||||
body: TCreateAcmeOrderPayload
|
||||
payload: TCreateAcmeOrderPayload
|
||||
): Promise<TCreateAcmeOrderResponse> => {
|
||||
const profile = await validateAcmeProfile(profileId);
|
||||
// FIXME: Implement ACME new order creation
|
||||
@@ -105,7 +126,7 @@ export const pkiAcmeServiceFactory = ({ certificateProfileDAL }: TPkiAcmeService
|
||||
const deactivateAcmeAccount = async (
|
||||
profileId: string,
|
||||
accountId: string,
|
||||
body?: TDeactivateAcmeAccountPayload
|
||||
payload?: TDeactivateAcmeAccountPayload
|
||||
): Promise<TDeactivateAcmeAccountResponse> => {
|
||||
const profile = await validateAcmeProfile(profileId);
|
||||
// FIXME: Implement ACME account deactivation
|
||||
@@ -137,10 +158,10 @@ export const pkiAcmeServiceFactory = ({ certificateProfileDAL }: TPkiAcmeService
|
||||
const finalizeAcmeOrder = async (
|
||||
profileId: string,
|
||||
orderId: string,
|
||||
body: TFinalizeAcmeOrderPayload
|
||||
payload: TFinalizeAcmeOrderPayload
|
||||
): Promise<TFinalizeAcmeOrderResponse> => {
|
||||
const profile = await validateAcmeProfile(profileId);
|
||||
const { csr } = body;
|
||||
const { csr } = payload;
|
||||
// FIXME: Implement ACME finalize order
|
||||
return {
|
||||
status: "processing",
|
||||
@@ -196,6 +217,7 @@ export const pkiAcmeServiceFactory = ({ certificateProfileDAL }: TPkiAcmeService
|
||||
};
|
||||
|
||||
return {
|
||||
validateCreateAcmeAccountJwsPayload,
|
||||
getAcmeDirectory,
|
||||
getAcmeNewNonce,
|
||||
createAcmeAccount,
|
||||
|
||||
@@ -12,7 +12,10 @@ import {
|
||||
GetAcmeAuthorizationResponseSchema,
|
||||
GetAcmeDirectoryResponseSchema,
|
||||
GetAcmeOrderResponseSchema,
|
||||
JwsPayloadSchema,
|
||||
ListAcmeOrdersResponseSchema,
|
||||
ProtectedHeaderSchema,
|
||||
RawJwsPayloadSchema,
|
||||
RespondToAcmeChallengeResponseSchema
|
||||
} from "./pki-acme-schemas";
|
||||
|
||||
@@ -28,12 +31,16 @@ export type TGetAcmeAuthorizationResponse = z.infer<typeof GetAcmeAuthorizationR
|
||||
export type TRespondToAcmeChallengeResponse = z.infer<typeof RespondToAcmeChallengeResponseSchema>;
|
||||
|
||||
// Payload types
|
||||
export type TRawJwsPayload = z.infer<typeof RawJwsPayloadSchema>;
|
||||
export type TJwsPayload = z.infer<typeof JwsPayloadSchema>;
|
||||
export type TProtectedHeader = z.infer<typeof ProtectedHeaderSchema>;
|
||||
export type TCreateAcmeAccountPayload = z.infer<typeof CreateAcmeAccountBodySchema>;
|
||||
export type TCreateAcmeOrderPayload = z.infer<typeof CreateAcmeOrderBodySchema>;
|
||||
export type TDeactivateAcmeAccountPayload = z.infer<typeof DeactivateAcmeAccountBodySchema>;
|
||||
export type TFinalizeAcmeOrderPayload = z.infer<typeof FinalizeAcmeOrderBodySchema>;
|
||||
|
||||
export type TPkiAcmeServiceFactory = {
|
||||
validateCreateAcmeAccountJwsPayload(body: TRawJwsPayload): Promise<TJwsPayload>;
|
||||
getAcmeDirectory: (profileId: string) => Promise<TGetAcmeDirectoryResponse>;
|
||||
getAcmeNewNonce: (profileId: string) => Promise<string>;
|
||||
createAcmeAccount: (profileId: string, body: TCreateAcmeAccountPayload) => Promise<TCreateAcmeAccountResponse>;
|
||||
|
||||
Reference in New Issue
Block a user