mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 11:27:32 +00:00
Implement eab verification
This commit is contained in:
@@ -549,3 +549,24 @@ export class AcmeBadCSRError extends AcmeError {
|
|||||||
this.name = "AcmeBadCSRError";
|
this.name = "AcmeBadCSRError";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class AcmeExternalAccountRequiredError extends AcmeError {
|
||||||
|
constructor({
|
||||||
|
detail = "External account binding is required",
|
||||||
|
error,
|
||||||
|
message
|
||||||
|
}: {
|
||||||
|
detail?: string;
|
||||||
|
error?: unknown;
|
||||||
|
message?: string;
|
||||||
|
} = {}) {
|
||||||
|
super({
|
||||||
|
type: AcmeErrorType.ExternalAccountRequired,
|
||||||
|
detail,
|
||||||
|
status: 400,
|
||||||
|
error,
|
||||||
|
message
|
||||||
|
});
|
||||||
|
this.name = "AcmeExternalAccountRequiredError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -66,13 +66,7 @@ export const CreateAcmeAccountBodySchema = z.object({
|
|||||||
contact: z.array(z.string()).optional(),
|
contact: z.array(z.string()).optional(),
|
||||||
termsOfServiceAgreed: z.boolean().optional(),
|
termsOfServiceAgreed: z.boolean().optional(),
|
||||||
onlyReturnExisting: z.boolean().optional(),
|
onlyReturnExisting: z.boolean().optional(),
|
||||||
externalAccountBinding: z
|
externalAccountBinding: RawJwsPayloadSchema.optional()
|
||||||
.object({
|
|
||||||
protected: z.string(),
|
|
||||||
payload: z.string(),
|
|
||||||
signature: z.string()
|
|
||||||
})
|
|
||||||
.optional()
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// New Account endpoint
|
// New Account endpoint
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { TPkiAcmeAccounts } from "@app/db/schemas/pki-acme-accounts";
|
|||||||
import { TPkiAcmeAuths } from "@app/db/schemas/pki-acme-auths";
|
import { TPkiAcmeAuths } from "@app/db/schemas/pki-acme-auths";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { crypto } from "@app/lib/crypto/cryptography";
|
import { crypto } from "@app/lib/crypto/cryptography";
|
||||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal";
|
import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal";
|
||||||
|
|
||||||
@@ -12,6 +12,9 @@ import {
|
|||||||
TCertificateProfileWithConfigs
|
TCertificateProfileWithConfigs
|
||||||
} from "@app/services/certificate-profile/certificate-profile-types";
|
} from "@app/services/certificate-profile/certificate-profile-types";
|
||||||
import { TCertificateV3ServiceFactory } from "@app/services/certificate-v3/certificate-v3-service";
|
import { TCertificateV3ServiceFactory } from "@app/services/certificate-v3/certificate-v3-service";
|
||||||
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
|
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
||||||
import {
|
import {
|
||||||
calculateJwkThumbprint,
|
calculateJwkThumbprint,
|
||||||
errors,
|
errors,
|
||||||
@@ -29,6 +32,7 @@ import {
|
|||||||
AcmeBadCSRError,
|
AcmeBadCSRError,
|
||||||
AcmeBadPublicKeyError,
|
AcmeBadPublicKeyError,
|
||||||
AcmeError,
|
AcmeError,
|
||||||
|
AcmeExternalAccountRequiredError,
|
||||||
AcmeMalformedError,
|
AcmeMalformedError,
|
||||||
AcmeOrderNotReadyError,
|
AcmeOrderNotReadyError,
|
||||||
AcmeServerInternalError,
|
AcmeServerInternalError,
|
||||||
@@ -67,8 +71,8 @@ import {
|
|||||||
} from "./pki-acme-types";
|
} from "./pki-acme-types";
|
||||||
|
|
||||||
type TPkiAcmeServiceFactoryDep = {
|
type TPkiAcmeServiceFactoryDep = {
|
||||||
certificateProfileDAL: Pick<TCertificateProfileDALFactory, "findByIdWithOwnerOrgId">;
|
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
|
||||||
certificateV3Service: Pick<TCertificateV3ServiceFactory, "signCertificateFromProfile">;
|
certificateProfileDAL: Pick<TCertificateProfileDALFactory, "findByIdWithOwnerOrgId" | "findByIdWithConfigs">;
|
||||||
acmeAccountDAL: Pick<
|
acmeAccountDAL: Pick<
|
||||||
TPkiAcmeAccountDALFactory,
|
TPkiAcmeAccountDALFactory,
|
||||||
"findByProjectIdAndAccountId" | "findByProfileIdAndPublicKeyThumbprintAndAlg" | "create"
|
"findByProjectIdAndAccountId" | "findByProfileIdAndPublicKeyThumbprintAndAlg" | "create"
|
||||||
@@ -83,21 +87,25 @@ type TPkiAcmeServiceFactoryDep = {
|
|||||||
TPkiAcmeChallengeDALFactory,
|
TPkiAcmeChallengeDALFactory,
|
||||||
"create" | "transaction" | "updateById" | "findByAccountAuthAndChallengeId" | "findByIdForChallengeValidation"
|
"create" | "transaction" | "updateById" | "findByAccountAuthAndChallengeId" | "findByIdForChallengeValidation"
|
||||||
>;
|
>;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
|
||||||
|
certificateV3Service: Pick<TCertificateV3ServiceFactory, "signCertificateFromProfile">;
|
||||||
acmeChallengeService: TPkiAcmeChallengeServiceFactory;
|
acmeChallengeService: TPkiAcmeChallengeServiceFactory;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const pkiAcmeServiceFactory = ({
|
export const pkiAcmeServiceFactory = ({
|
||||||
|
projectDAL,
|
||||||
certificateProfileDAL,
|
certificateProfileDAL,
|
||||||
certificateV3Service,
|
|
||||||
acmeAccountDAL,
|
acmeAccountDAL,
|
||||||
acmeOrderDAL,
|
acmeOrderDAL,
|
||||||
acmeAuthDAL,
|
acmeAuthDAL,
|
||||||
acmeOrderAuthDAL,
|
acmeOrderAuthDAL,
|
||||||
acmeChallengeDAL,
|
acmeChallengeDAL,
|
||||||
|
kmsService,
|
||||||
|
certificateV3Service,
|
||||||
acmeChallengeService
|
acmeChallengeService
|
||||||
}: TPkiAcmeServiceFactoryDep): TPkiAcmeServiceFactory => {
|
}: TPkiAcmeServiceFactoryDep): TPkiAcmeServiceFactory => {
|
||||||
const validateAcmeProfile = async (profileId: string): Promise<TCertificateProfileWithConfigs> => {
|
const validateAcmeProfile = async (profileId: string): Promise<TCertificateProfileWithConfigs> => {
|
||||||
const profile = await certificateProfileDAL.findById(profileId);
|
const profile = await certificateProfileDAL.findByIdWithConfigs(profileId);
|
||||||
if (!profile) {
|
if (!profile) {
|
||||||
throw new NotFoundError({ message: "Certificate profile not found" });
|
throw new NotFoundError({ message: "Certificate profile not found" });
|
||||||
}
|
}
|
||||||
@@ -304,7 +312,7 @@ export const pkiAcmeServiceFactory = ({
|
|||||||
profileId,
|
profileId,
|
||||||
alg,
|
alg,
|
||||||
jwk,
|
jwk,
|
||||||
payload: { onlyReturnExisting, contact }
|
payload: { onlyReturnExisting, contact, externalAccountBinding }
|
||||||
}: {
|
}: {
|
||||||
profileId: string;
|
profileId: string;
|
||||||
alg: string;
|
alg: string;
|
||||||
@@ -312,6 +320,44 @@ export const pkiAcmeServiceFactory = ({
|
|||||||
payload: TCreateAcmeAccountPayload;
|
payload: TCreateAcmeAccountPayload;
|
||||||
}): Promise<TAcmeResponse<TCreateAcmeAccountResponse>> => {
|
}): Promise<TAcmeResponse<TCreateAcmeAccountResponse>> => {
|
||||||
const profile = await validateAcmeProfile(profileId);
|
const profile = await validateAcmeProfile(profileId);
|
||||||
|
if (!externalAccountBinding) {
|
||||||
|
throw new AcmeExternalAccountRequiredError({ detail: "External account binding is required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
|
||||||
|
projectId: profile.projectId,
|
||||||
|
projectDAL,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
const kmsDecryptor = await kmsService.decryptWithKmsKey({
|
||||||
|
kmsId: certificateManagerKmsId
|
||||||
|
});
|
||||||
|
const eabSecret = await kmsDecryptor({ cipherTextBlob: profile.acmeConfig!.encryptedEabSecret });
|
||||||
|
const encodedSecret = new TextEncoder().encode(eabSecret.toString());
|
||||||
|
try {
|
||||||
|
const { payload: eabPayload, protectedHeader: eabProtectedHeader } = await flattenedVerify(
|
||||||
|
externalAccountBinding,
|
||||||
|
encodedSecret
|
||||||
|
);
|
||||||
|
const alg = eabProtectedHeader!.alg!;
|
||||||
|
if (!["HS256", "HS384", "HS512"].includes(alg)) {
|
||||||
|
throw new AcmeMalformedError({ detail: "Invalid algorithm for external account binding JWS payload" });
|
||||||
|
}
|
||||||
|
if ((eabPayload as unknown as { kid: string }).kid !== profile.id) {
|
||||||
|
throw new UnauthorizedError({ message: "External account binding KID mismatch" });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof errors.JWSInvalid) {
|
||||||
|
throw new AcmeMalformedError({ detail: "Invalid external account binding JWS payload" });
|
||||||
|
}
|
||||||
|
if (error instanceof AcmeError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
logger.error(error, "Unexpected error while verifying EAB JWS payload");
|
||||||
|
throw new AcmeServerInternalError({ detail: "Failed to verify EAB JWS payload" });
|
||||||
|
}
|
||||||
|
|
||||||
const publicKeyThumbprint = await calculateJwkThumbprint(jwk, "sha256");
|
const publicKeyThumbprint = await calculateJwkThumbprint(jwk, "sha256");
|
||||||
const existingAccount: TPkiAcmeAccounts | null = await acmeAccountDAL.findByProfileIdAndPublicKeyThumbprintAndAlg(
|
const existingAccount: TPkiAcmeAccounts | null = await acmeAccountDAL.findByProfileIdAndPublicKeyThumbprintAndAlg(
|
||||||
profileId,
|
profileId,
|
||||||
|
|||||||
Reference in New Issue
Block a user