我正在尝试使用 Web Crypto API 对 CloudKit Web 服务请求进行身份验证。
我已经使用节点加密 API 进行了身份验证,并已验证其有效:
import Crypto from 'crypto'
export async function cloudKitRequest(body, method, operationSubpath) {
const subpath = `/database/1/${env.CLOUDKIT_CONTAINER}/${env.CLOUDKIT_ENVIRONMENT}/${operationSubpath}`
const bodyString = JSON.stringify(body)
const hash = Crypto.createHash('sha256')
const sign = Crypto.createSign('SHA256')
const date = new Date().toISOString().replace(/\.[0-9]+?Z/, 'Z')
hash.update(bodyString, 'utf8')
sign.update(`${date}:${hash.digest('base64')}:${subpath}`)
return await fetch(`https://api.apple-cloudkit.com${subpath}`, {
body: bodyString,
method: method,
headers: {
'Content-Type': 'application/json',
'X-Apple-Cloudkit-Request-KeyID': env.CLOUDKIT_KEY_ID,
'X-Apple-CloudKit-Request-ISO8601Date': date,
'X-Apple-CloudKit-Request-SignatureV1': sign.sign(
env.CLOUDKIT_PRIVATE_KEY,
'base64'
),
},
})
}
我现在已将其转换为使用 Web Crypto API,但收到“身份验证失败”错误,表明我的请求签名错误。我已经验证我的正文哈希是正确的。这是我的网络加密代码:
const encoder = new TextEncoder()
export async function cloudKitRequest(body: any, method: string, operationSubpath: string, env: any) {
const subpath = `/database/1/${env.CLOUDKIT_CONTAINER}/${env.CLOUDKIT_ENVIRONMENT}/${operationSubpath}`
const date = new Date().toISOString().replace(/\.[0-9]+?Z/, 'Z')
const bodyHash = await hashBody(body)
const message = `${date}:${bodyHash}:${subpath}`
const privateKey = await loadPrivateKey(env.CLOUDKIT_PRIVATE_KEY)
const signature = await signMessage(privateKey, message)
const headers = new Headers({
'Content-Type': 'application/json',
'X-Apple-CloudKit-Request-KeyID': env.CLOUDKIT_KEY_ID,
'X-Apple-CloudKit-Request-ISO8601Date': date,
'X-Apple-CloudKit-Request-SignatureV1': signature,
})
const options = {
method: method,
headers: headers,
body: JSON.stringify(body),
}
return await fetch(`https://api.apple-cloudkit.com${subpath}`, options)
}
const hashBody = async (requestBody: any) => {
const encodedBody = encoder.encode(JSON.stringify(requestBody))
const hashBuffer = await crypto.subtle.digest('SHA-256', encodedBody)
return btoa(String.fromCharCode(...new Uint8Array(hashBuffer)))
}
const b642ab = (base64_string: string) => {
return Uint8Array.from(atob(base64_string), (c) => c.charCodeAt(0))
}
const signMessage = async (privateKey: CryptoKey, message: string) => {
const encoder = new TextEncoder()
const encodedMessage = encoder.encode(message)
const signature = await crypto.subtle.sign(
{
name: 'ECDSA',
hash: { name: 'SHA-256' },
},
privateKey,
encodedMessage
)
return btoa(String.fromCharCode(...new Uint8Array(signature)))
}
const loadPrivateKey = async (pem: string) => {
const binaryDer = b642ab(pem)
const importParams = {
name: 'ECDSA',
namedCurve: 'P-256',
}
return await crypto.subtle.importKey('pkcs8', binaryDer.buffer, importParams, true, ['sign'])
}
按照此处的指导,我的私钥是使用
openssl ecparam -name prime256v1 -genkey -noout -out eckey.pem
生成的。在节点加密示例中,env.CLOUDKIT_PRIVATE_KEY
设置为 eckey.pem
的确切内容并且可以正常工作。在网络加密示例中,我尝试了这两个内容并使用 pkcs8
转换为 openssl pkcs8 -topk8 -nocrypt -in eckey.pem -out eckey_okcs8.pem
,然后将 env.CLOUDKIT_PRIVATE_KEY
设置为内容,删除标题/后缀和任何换行符。
CloudKit 文档非常模糊,我不确定我到底做错了什么。
调用示例:
await cloudKitRequest({
operations: {
operationType: 'create',
recordName: 'randomuuid',
record: {
recordType: 'MyRecord',
fields: {
name: {
value: 'Test Name',
}
},
},
},
},
'POST',
'public/records/modify',
context.env
)
正如发现的这里,
WebCrypto 代码的问题在于它生成 P1363 格式的签名,而 CloudKit API 需要 ASN.1/DER 格式的签名。生成签名后转换为 DER 修复了身份验证失败错误。
转换代码示例:
/**
* Converts a P1363 signature to ASN.1/DER format
* @param {Uint8Array} signature - The P1363 signature to convert
* @returns {Uint8Array} The ASN.1/DER formatted signature
*/
function convertToDER(signature) {
const r = signature.slice(0, 32)
const s = signature.slice(32, 64)
const rDer = toDERElement(r)
const sDer = toDERElement(s)
// Construct the DER sequence bytes
const derSequenceLength = rDer.length + sDer.length + 2 // 2 extra bytes for DER sequence header
const derArray = new Uint8Array(derSequenceLength)
derArray[0] = 0x30 // DER sequence tag
derArray[1] = rDer.length + sDer.length // Length of the sequence
derArray.set(rDer, 2)
derArray.set(sDer, 2 + rDer.length)
return derArray
}
/**
* Converts a single component of a P1363 signature to a DER-encoded element
* @param {Uint8Array} component - The component to convert (either r or s)
* @returns {Uint8Array} The DER-encoded element
*/
function toDERElement(component) {
const leadingZero = (component[0] & 0x80) !== 0
const length = component.length + (leadingZero ? 1 : 0)
const derElement = new Uint8Array(length + 2) // 2 bytes for DER integer header
derElement[0] = 0x02 // DER integer tag
derElement[1] = length // Length of the integer
if (leadingZero) {
derElement[2] = 0x00
derElement.set(component, 3)
} else {
derElement.set(component, 2)
}
return derElement
}
更新了签名消息:
const signMessage = async (privateKey: CryptoKey, message: string) => {
const encoder = new TextEncoder()
const encodedMessage = encoder.encode(message)
const p1363Signature = await crypto.subtle.sign(
{
name: 'ECDSA',
hash: { name: 'SHA-256' },
},
privateKey,
encodedMessage
)
// Convert P1363 format to ASN.1/DER format
const derSignature = convertToDER(new Uint8Array(p1363Signature))
return btoa(String.fromCharCode(...new Uint8Array(derSignature)))
}