supabase-完整性测试
苹果App Attest服务器端用于边缘运行时间的验证,只使用WebCrypto.
安装
# Deno
deno add jsr:@bradford-tech/supabase-integrity-attest
# npm
npm install @bradford-tech/supabase-integrity-attest
快开始
校验设备验证并提取其公钥 :
import { verifyAttestation } from "@bradford-tech/supabase-integrity-attest";
const clientDataHash = new Uint8Array(
await crypto.subtle.digest("SHA-256", new TextEncoder().encode(challenge)),
);
const { publicKeyPem, signCount } = await verifyAttestation(
{ appId: "TEAMID.com.example.app" },
keyId, // base64 key identifier from client
clientDataHash, // SHA-256 of the challenge you issued
attestation, // base64 CBOR attestation from client
);
// publicKeyPem: "-----BEGIN PUBLIC KEY-----\nMFkw..."
// signCount: 0
存储 publicKeyPem 财务报告和审定财务报表 signCount 用于此设备。 使用它们来验证未来的断言.
为什么这个图书馆
现有的 App Attest 校验库依赖于 node:crypto 或者在边缘运行时崩溃的软件包. appattest-checker-node 用途 X509Certificate.verify(),该投出 ERR_NOT_IMPLEMENTED 在德诺. pkijs 在 Supabase Edge 函数的模块装入时崩溃, 因为 self.crypto.name 未定义. @peculiar/x509 停车 tsyringe 财务报告和审定财务报表 reflect-metadata,在模块初始化时依赖于全球副作用.
此库只使用 crypto.subtle 密码操作 asn1js x.509 解析和 @noble/curves 用于一个操作Deno's WebCrypto不支持(苹果公司中间证书上的P-384签名验证).
中间软件
下面的两个中间软件包都使用Supabase服务-作用客户端来访问数据库:
import { createClient } from "jsr:@supabase/supabase-js@2";
const supabase = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!,
);
withAttestation -- 设备注册
withAttestation 将一次性设备注册端口与自动挑战消耗、验证和设备密钥存储相包裹:
import { withAttestation } from "@bradford-tech/supabase-integrity-attest";
Deno.serve(withAttestation({
appId: Deno.env.get("APP_ATTEST_APP_ID")!,
consumeChallenge: async (challenge) => {
const id = new TextDecoder().decode(challenge);
const { data } = await supabase
.from("attestation_challenges")
.delete()
.eq("id", id)
.select("id");
return (data?.length ?? 0) > 0;
},
storeDeviceKey: async ({ deviceId, publicKeyPem, signCount }) => {
await supabase
.from("device_attestations")
.upsert({ key_id: deviceId, public_key_pem: publicKeyPem, sign_count: signCount });
},
}, (_req, ctx) => {
// ctx.deviceId, ctx.publicKeyPem, ctx.signCount, ctx.receipt, ctx.timings
return Response.json({ deviceId: ctx.deviceId });
}));
consumeChallenge 必须是原子:返回 true 如果挑战有效、未使用和未到期(现已耗尽), false 否则。 使用 DELETE ... RETURNING 保证单用语义.
默认提取器读取一个JSON体:
POST /functions/v1/attest
Content-Type: application/json
{"keyId": "<base64>", "challenge": "<base64>", "attestation": "<base64>"}
withAssertion -- 受保护的请求
withAssertion 将任何受保护的端点以自动确认、设备密钥查找并签名计数承诺来包装:
import { withAssertion } from "@bradford-tech/supabase-integrity-attest";
Deno.serve(withAssertion({
appId: Deno.env.get("APP_ATTEST_APP_ID")!,
getDeviceKey: async (deviceId) => {
const { data } = await supabase
.from("device_attestations")
.select("public_key_pem, sign_count")
.eq("key_id", deviceId)
.single();
return data
? { publicKeyPem: data.public_key_pem, signCount: data.sign_count }
: null;
},
commitSignCount: async (deviceId, newSignCount) => {
const { data } = await supabase
.from("device_attestations")
.update({ sign_count: newSignCount })
.eq("key_id", deviceId)
.lt("sign_count", newSignCount)
.select("key_id");
return (data?.length ?? 0) > 0;
},
}, async (_req, { rawBody }) => {
const payload = JSON.parse(new TextDecoder().decode(rawBody));
return Response.json({ ok: true });
}));
客户端在信头中发送断言和装置ID. 请求机构是签名客户端数据:
POST /functions/v1/your-endpoint
X-App-Attest-Assertion: <base64-encoded assertion>
X-App-Attest-Device-Id: <base64-encoded keyId>
Content-Type: application/json
{"text": "Hello world", "voice": "en-US"}
签名计数解剖
commitSignCount 必须 使用比较和扫描:如果当前值严格低于 newSignCount无条件 UPDATE ... SET sign_count = $1 当两个请求同时到达时, 静静地中断了 重放保护 .
UPDATE device_attestations
SET sign_count = $1, last_seen_at = now()
WHERE key_id = $2 AND sign_count < $1
返回时 true 如果行已更新。 图书馆转换 false 输入 AssertionError(SIGN_COUNT_STALE)。 。 。 .
共享选项
一旦您拥有多个受保护的功能, 请提取共享选项 :
// supabase/functions/_shared/attest.ts
import type { WithAssertionOptions } from "@bradford-tech/supabase-integrity-attest";
export const assertionOptions: WithAssertionOptions = {
appId: Deno.env.get("APP_ATTEST_APP_ID")!,
// ... getDeviceKey, commitSignCount as above
};
// supabase/functions/text-to-speech/index.ts
import { withAssertion } from "@bradford-tech/supabase-integrity-attest";
import { assertionOptions } from "../_shared/attest.ts";
Deno.serve(withAssertion(assertionOptions, async (_req, { rawBody }) => {
const { text, voice } = JSON.parse(new TextDecoder().decode(rawBody));
return Response.json({ audio: "..." });
}));
Supabase 适配器
在Supabase上,跳过完全写回调. 这个 /supabase 子路径舰 createSupabaseAdapter(),它提供了所有四个存储回调 issueChallenge(),辅以两个邮递表:
import { withAttestation } from "@bradford-tech/supabase-integrity-attest/attestation";
import { createSupabaseAdapter } from "@bradford-tech/supabase-integrity-attest/supabase";
const adapter = createSupabaseAdapter(supabase); // service-role client
// Challenge endpoint
const { challengeBase64, expiresAt } = await adapter.issueChallenge("attestation");
// Registration endpoint -- adapter supplies consumeChallenge + storeDeviceKey
Deno.serve(withAttestation(
{ appId: Deno.env.get("APP_ATTEST_APP_ID")!, ...adapter },
(_req, ctx) => Response.json({ deviceId: ctx.deviceId }),
));
// Protected endpoints -- adapter supplies getDeviceKey + commitSignCount
// withAssertion({ appId, ...adapter }, handler)
装有包裹的舰艇 sql/app_attest.sql (两个表格, RLS 锁定到服务角色, 过期索引, pg cron swearing of 过期的挑战). 表格名称和质疑 TTL 通过选项参数可以配置 . @supabase/supabase-js 不是一个依赖性——客户端在结构上被打入,所以任何最近的客户端都会工作.
App Attest 证明了设备的完整性,而不是用户身份:适配器和中间软件与Supabase Auth,AWS Cognitto,或任何无记名JWT并列.
低级 API
为了全面控制核查流量,使用 verifyAttestation 财务报告和审定财务报表 verifyAssertion 直接说.
上面的快速开始显示 verifyAttestation。注意: clientDataHash 必须是SHA-256的挑战,而不是原始的挑战。 客户端 SDKs( Expo) attestKeyAsync,当地 DCAppAttestService.attestKey)在传递到苹果公司之前先在内部将挑战进行散列;您必须生产相同的散列服务器侧. 这个 withAttestation 中件自动处理此操作 .
请求
import { verifyAssertion } from "@bradford-tech/supabase-integrity-attest";
const { signCount } = await verifyAssertion(
{ appId: "TEAMID.com.example.app" },
assertion, // base64 CBOR from client
clientData, // the request payload that was signed
storedPublicKeyPem,
storedSignCount,
);
// Update stored signCount to signCount
子路径导入
仅导入您需要的缩放捆绑大小 :
// Full library (attestation + assertion)
import { verifyAttestation, verifyAssertion } from "@bradford-tech/supabase-integrity-attest";
// Assertion only -- skips asn1js and @noble/curves
import { verifyAssertion, withAssertion } from "@bradford-tech/supabase-integrity-attest/assertion";
// Attestation only
import { verifyAttestation, withAttestation } from "@bradford-tech/supabase-integrity-attest/attestation";
// Supabase storage adapter
import { createSupabaseAdapter } from "@bradford-tech/supabase-integrity-attest/supabase";
处理错误
import {
AttestationError,
AssertionError,
} from "@bradford-tech/supabase-integrity-attest";
try {
await verifyAttestation(appInfo, keyId, clientDataHash, attestation);
} catch (e) {
if (e instanceof AttestationError) {
console.log(e.code);
// => "NONCE_MISMATCH"
}
}
验证错误代码
| 代码 | 含义 |
|---|---|
INVALID_FORMAT | CBOR 解码或结构验证失败 |
INVALID_CERTIFICATE_CHAIN | X.509 证书链验证失败 |
NONCE_MISMATCH | 计算出的非ce 不匹配证书的非ce |
RP_ID_MISMATCH | RP ID 散列不符合应用程序 ID 的 SHA-256 |
KEY_ID_MISMATCH | 公钥散列不符合所提供的密钥 ID |
INVALID_COUNTER | 签名计数不是零( 需要证明) |
INVALID_AAGUID | AAGUID 不符合预期环境 |
CHALLENGE_INVALID | 缺少、过期或已经耗用的挑战withAttestation 仅限) |
INTERNAL_ERROR | 存储回调或内部出错( Q)withAttestation 仅限) |
请求错误代码
| 代码 | 含义 |
|---|---|
INVALID_FORMAT | CBOR 解码或结构验证失败 |
RP_ID_MISMATCH | RP ID 散列不符合应用程序 ID 的 SHA-256 |
COUNTER_NOT_INCREMENTED | 签名计数不大于存储值 |
SIGNATURE_INVALID | ECDSA 签名验证失败 |
DEVICE_NOT_FOUND | 没有给定设备ID的设备密钥( N)withAssertion 仅限) |
INTERNAL_ERROR | 存储回调或内部出错( Q)withAssertion 仅限) |
SIGN_COUNT_STALE | 同时提出的请求已经将计数器提前( Q)withAssertion 仅限) |
发展环境
对于使用苹果开发的 App Attest 环境的应用程序,通过 developmentEnv: true数字 :
await verifyAttestation(
{ appId: "TEAMID.com.example.app", developmentEnv: true },
keyId,
clientDataHash,
attestation,
);
withAttestation 还接受 developmentEnv 在它的选择.
文档
页:1 技术。 。 。 .
捐款
欢迎提出问题和拉动请求 GitHub 图像。 。 。 .
警卫
见 警卫 脆弱性报告.
许可证
麻省理工学院