ox
Version:
Ethereum Standard Library
222 lines (202 loc) • 6.56 kB
text/typescript
import * as Bytes from './Bytes.js'
import * as Cbor from './Cbor.js'
import * as Errors from './Errors.js'
import type * as Hex from './Hex.js'
import * as Cursor from './internal/cursor.js'
import * as PublicKey from './PublicKey.js'
/**
* Converts a P256 {@link ox#PublicKey.PublicKey} to a CBOR-encoded COSE_Key.
*
* The COSE_Key uses integer map keys per [RFC 9053](https://datatracker.ietf.org/doc/html/rfc9053):
* - `1` (kty): `2` (EC2)
* - `3` (alg): `-7` (ES256)
* - `-1` (crv): `1` (P-256)
* - `-2` (x): x coordinate bytes
* - `-3` (y): y coordinate bytes
*
* @example
* ```ts twoslash
* import { CoseKey, P256 } from 'ox'
*
* const { publicKey } = P256.createKeyPair()
*
* const coseKey = CoseKey.fromPublicKey(publicKey)
* ```
*
* @param publicKey - The P256 public key to convert.
* @returns The CBOR-encoded COSE_Key as a Hex string.
*/
export function fromPublicKey(publicKey: PublicKey.PublicKey): Hex.Hex {
const pkBytes = PublicKey.toBytes(publicKey)
const x = pkBytes.slice(1, 33)
const y = pkBytes.slice(33, 65)
return Cbor.encode(
new Map<number, unknown>([
[1, 2], // kty: EC2
[3, -7], // alg: ES256
[-1, 1], // crv: P-256
[-2, x], // x coordinate
[-3, y], // y coordinate
]),
)
}
export declare namespace fromPublicKey {
type ErrorType =
| PublicKey.toBytes.ErrorType
| Cbor.encode.ErrorType
| Errors.GlobalErrorType
}
/**
* Converts a CBOR-encoded COSE_Key to a P256 {@link ox#PublicKey.PublicKey}.
*
* Accepts the COSE key as either a hex string or raw bytes. When the
* `returnByteLength` or `returnDecoded` option is set, the function returns an
* object containing the public key plus the consumed byte length and/or the
* decoded CBOR map. This is useful for parsing CBOR streams (such as WebAuthn
* `authenticatorData`) where the COSE key is followed by trailing data.
*
* @example
* ```ts twoslash
* import { CoseKey, P256 } from 'ox'
*
* const { publicKey } = P256.createKeyPair()
* const coseKey = CoseKey.fromPublicKey(publicKey)
*
* const publicKey2 = CoseKey.toPublicKey(coseKey)
* ```
*
* @example
* ### With Byte Length
*
* ```ts twoslash
* import { CoseKey, P256 } from 'ox'
*
* const { publicKey } = P256.createKeyPair()
* const coseKey = CoseKey.fromPublicKey(publicKey)
*
* const { publicKey: pk, byteLength } = CoseKey.toPublicKey(
* coseKey,
* {
* returnByteLength: true
* }
* )
* ```
*
* @param coseKey - The CBOR-encoded COSE_Key as hex or bytes.
* @param options - Decoding options.
* @returns The P256 public key, optionally with byte length and decoded CBOR.
*/
export function toPublicKey<options extends toPublicKey.Options = {}>(
coseKey: Hex.Hex | Uint8Array,
options: options | toPublicKey.Options = {},
): toPublicKey.ReturnType<options> {
const bytes = typeof coseKey === 'string' ? Bytes.fromHex(coseKey) : coseKey
const cursor = Cursor.create(bytes)
const decoded = Cbor.decode<Record<string, unknown>>(bytes)
// After Cbor.decode, the cursor's position would advance, but Cbor.decode
// creates its own cursor internally. Walk the bytes here to compute consumed
// byte length without re-encoding (which loses optional COSE members).
cursor.position = 0
cborSkip(cursor)
const byteLength = cursor.position
// Validate COSE_Key header per RFC 9053 (kty=2 EC2, alg=-7 ES256, crv=1 P-256).
if (decoded['1'] !== 2 || decoded['3'] !== -7 || decoded['-1'] !== 1)
throw new InvalidCoseKeyError()
const x = decoded['-2']
const y = decoded['-3']
if (
!(x instanceof Uint8Array) ||
x.length !== 32 ||
!(y instanceof Uint8Array) ||
y.length !== 32
)
throw new InvalidCoseKeyError()
const raw = new Uint8Array(65)
raw[0] = 0x04
raw.set(x, 1)
raw.set(y, 33)
const publicKey = PublicKey.from(raw)
if (!options.returnByteLength && !options.returnDecoded)
return publicKey as toPublicKey.ReturnType<options>
const result: toPublicKey.ResultObject = { publicKey }
if (options.returnByteLength) result.byteLength = byteLength
if (options.returnDecoded) result.decoded = decoded
return result as toPublicKey.ReturnType<options>
}
export declare namespace toPublicKey {
type Options = {
/** Include the consumed byte length of the COSE key in the result. */
returnByteLength?: boolean | undefined
/** Include the decoded CBOR map in the result. */
returnDecoded?: boolean | undefined
}
type ResultObject = {
publicKey: PublicKey.PublicKey
byteLength?: number
decoded?: Record<string, unknown>
}
type ReturnType<options extends Options = {}> =
options['returnByteLength'] extends true
? ResultObject
: options['returnDecoded'] extends true
? ResultObject
: PublicKey.PublicKey
type ErrorType =
| Bytes.fromHex.ErrorType
| Cbor.decode.ErrorType
| PublicKey.from.ErrorType
| InvalidCoseKeyError
| Errors.GlobalErrorType
}
/**
* Walks one CBOR element on the cursor, advancing `cursor.position` past it.
* Supports the major types used by COSE keys: unsigned int (0), negative int
* (1), byte string (2), text string (3), array (4), map (5).
*
* @internal
*/
function cborSkip(cursor: Cursor.Cursor): void {
const initialByte = cursor.readUint8()
const majorType = initialByte >> 5
const additionalInfo = initialByte & 0b00011111
const length = cborReadLength(cursor, additionalInfo)
switch (majorType) {
case 0:
case 1:
// value already consumed by `cborReadLength`.
return
case 2:
case 3:
// skip `length` bytes of byte/text string content
cursor.readBytes(length)
return
case 4:
for (let i = 0; i < length; i++) cborSkip(cursor)
return
case 5:
for (let i = 0; i < length; i++) {
cborSkip(cursor)
cborSkip(cursor)
}
return
case 7:
return
default:
throw new InvalidCoseKeyError()
}
}
/** @internal */
function cborReadLength(cursor: Cursor.Cursor, ai: number): number {
if (ai < 24) return ai
if (ai === 24) return cursor.readUint8()
if (ai === 25) return cursor.readUint16()
if (ai === 26) return cursor.readUint32()
throw new InvalidCoseKeyError()
}
/** Thrown when a COSE_Key does not contain valid P256 public key coordinates. */
export class InvalidCoseKeyError extends Errors.BaseError {
override readonly name = 'CoseKey.InvalidCoseKeyError'
constructor() {
super('COSE_Key does not contain valid P256 public key coordinates.')
}
}