使用 Web Crypto API 签署 CloudKit Web 服务请求

问题描述 投票:0回答:1

我正在尝试使用 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
)
javascript cryptography webcrypto-api cloudkit-web-services
1个回答
0
投票

正如发现的这里

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)))
}
© www.soinside.com 2019 - 2024. All rights reserved.