@atproto/jwk
Version:
A library for working with JSON Web Keys (JWKs) in TypeScript. This is meant to be extended by environment-specific libraries like @atproto/jwk-jose.
201 lines • 7.46 kB
JavaScript
import { z } from 'zod';
import { isLastOccurrence } from './util.js';
export const PUBLIC_KEY_USAGE = ['verify', 'encrypt', 'wrapKey'];
export const publicKeyUsageSchema = z.enum(PUBLIC_KEY_USAGE);
export function isPublicKeyUsage(usage) {
return PUBLIC_KEY_USAGE.includes(usage);
}
/**
* Determines if the given key usage is consistent for "sig" (signature) public
* key use.
*/
export function isSigKeyUsage(v) {
return v === 'verify';
}
/**
* Determines if the given key usage is consistent for "enc" (encryption) public
* key use.
*
* > When a key is used to wrap another key and a public key use
* > designation for the first key is desired, the "enc" (encryption)
* > key use value is used, since key wrapping is a kind of encryption.
* > The "enc" value is also to be used for public keys used for key
* > agreement operations.
* @see {@link https://datatracker.ietf.org/doc/html/rfc7517#section-4.2}
*/
export function isEncKeyUsage(v) {
return v === 'encrypt' || v === 'wrapKey';
}
export const PRIVATE_KEY_USAGE = [
'sign',
'decrypt',
'unwrapKey',
'deriveKey',
'deriveBits',
];
export const privateKeyUsageSchema = z.enum(PRIVATE_KEY_USAGE);
export function isPrivateKeyUsage(usage) {
return PRIVATE_KEY_USAGE.includes(usage);
}
export const KEY_USAGE = [...PRIVATE_KEY_USAGE, ...PUBLIC_KEY_USAGE];
export const keyUsageSchema = z.enum(KEY_USAGE);
/**
* @see {@link https://datatracker.ietf.org/doc/html/rfc7517#section-4 JSON Web Key (JWK) Format}
* @see {@link https://www.iana.org/assignments/jose/jose.xhtml#web-key-parameters IANA "JSON Web Key Parameters" registry}
*/
const jwkBaseSchema = z.object({
kty: z.string().min(1),
alg: z.string().min(1).optional(),
kid: z.string().min(1).optional(),
use: z.enum(['sig', 'enc']).optional(),
key_ops: z
.array(keyUsageSchema)
.min(1, { message: 'At least one key usage must be specified' })
// https://datatracker.ietf.org/doc/html/rfc7517#section-4.3
// > Duplicate key operation values MUST NOT be present in the array.
.refine((ops) => ops.every(isLastOccurrence), {
message: 'key_ops must not contain duplicates',
})
.optional(),
x5c: z.array(z.string()).optional(), // X.509 Certificate Chain
x5t: z.string().min(1).optional(), // X.509 Certificate SHA-1 Thumbprint
'x5t#S256': z.string().min(1).optional(), // X.509 Certificate SHA-256 Thumbprint
x5u: z.string().url().optional(), // X.509 URL
// https://www.w3.org/TR/webcrypto/
ext: z.boolean().optional(), // Extractable
// Federation Historical Keys Response
// https://openid.net/specs/openid-federation-1_0.html#name-federation-historical-keys-res
iat: z.number().int().optional(), // Issued At (timestamp)
exp: z.number().int().optional(), // Expiration Time (timestamp)
nbf: z.number().int().optional(), // Not Before (timestamp)
revoked: z // properties of the revocation
.object({
revoked_at: z.number().int(),
reason: z.string().optional(),
})
.optional(),
});
const jwkRsaKeySchema = jwkBaseSchema.extend({
kty: z.literal('RSA'),
alg: z
.enum(['RS256', 'RS384', 'RS512', 'PS256', 'PS384', 'PS512'])
.optional(),
n: z.string().min(1), // Modulus
e: z.string().min(1), // Exponent
d: z.string().min(1).optional(), // Private Exponent
p: z.string().min(1).optional(), // First Prime Factor
q: z.string().min(1).optional(), // Second Prime Factor
dp: z.string().min(1).optional(), // First Factor CRT Exponent
dq: z.string().min(1).optional(), // Second Factor CRT Exponent
qi: z.string().min(1).optional(), // First CRT Coefficient
oth: z
.array(z.object({
r: z.string().optional(),
d: z.string().optional(),
t: z.string().optional(),
}))
.min(1)
.optional(), // Other Primes Info
});
const jwkEcKeySchema = jwkBaseSchema.extend({
kty: z.literal('EC'),
alg: z.enum(['ES256', 'ES384', 'ES512']).optional(),
crv: z.enum(['P-256', 'P-384', 'P-521']),
x: z.string().min(1),
y: z.string().min(1),
d: z.string().min(1).optional(), // ECC Private Key
});
const jwkEcSecp256k1KeySchema = jwkBaseSchema.extend({
kty: z.literal('EC'),
alg: z.enum(['ES256K']).optional(),
crv: z.enum(['secp256k1']),
x: z.string().min(1),
y: z.string().min(1),
d: z.string().min(1).optional(), // ECC Private Key
});
const jwkOkpKeySchema = jwkBaseSchema.extend({
kty: z.literal('OKP'),
alg: z.enum(['EdDSA']).optional(),
crv: z.enum(['Ed25519', 'Ed448']),
x: z.string().min(1),
d: z.string().min(1).optional(), // ECC Private Key
});
const jwkSymKeySchema = jwkBaseSchema.extend({
kty: z.literal('oct'), // Octet Sequence (used to represent symmetric keys)
alg: z.enum(['HS256', 'HS384', 'HS512']).optional(),
k: z.string(), // Key Value (base64url encoded)
});
/**
* Zod parser for known JWK types
*/
export const jwkSchema = z
.union([
jwkRsaKeySchema,
jwkEcKeySchema,
jwkEcSecp256k1KeySchema,
jwkOkpKeySchema,
jwkSymKeySchema,
])
// @TODO These rules should be applied to jwkBaseSchema, but Zod 3 doesn't
// support extending refined schemas. Move these to the base schema when we
// upgrade to Zod 4.
.refine(
// https://datatracker.ietf.org/doc/html/rfc7517#section-4.2
// > The "use" (public key use) parameter identifies the intended use of the
// > public key
(k) => k.use == null || isPublicJwk(k), {
message: '"use" can only be used with public keys',
path: ['use'],
})
.refine((k) => !k.key_ops?.some(isPrivateKeyUsage) || isPrivateJwk(k), {
message: 'private key usage not allowed for public keys',
path: ['key_ops'],
})
.refine(
// https://datatracker.ietf.org/doc/html/rfc7517#section-4.3
// > The "use" and "key_ops" JWK members SHOULD NOT be used together;
// > however, if both are used, the information they convey MUST be
// > consistent.
(k) => k.use == null ||
k.key_ops == null ||
(k.use === 'sig' && k.key_ops.every(isSigKeyUsage)) ||
(k.use === 'enc' && k.key_ops.every(isEncKeyUsage)), {
message: '"key_ops" must be consistent with "use"',
path: ['key_ops'],
});
/** @deprecated use {@link jwkSchema} instead */
export const jwkValidator = jwkSchema;
export const jwkPubSchema = jwkSchema
.refine(hasKid, {
message: '"kid" is required',
path: ['kid'],
})
// @NOTE for legacy reasons, we don't impose the presence of either "use" or "key_ops"
.refine(isPublicJwk, {
message: 'private key not allowed',
})
.refine((k) => !k.key_ops || k.key_ops.every(isPublicKeyUsage), {
message: '"key_ops" must not contain private key usage for public keys',
path: ['key_ops'],
});
export const jwkPrivateSchema = jwkSchema
// @NOTE we don't impose the presence of "kid"
.refine(isPrivateJwk, {
message: 'private key required',
});
export function hasKid(jwk) {
return 'kid' in jwk && jwk.kid != null;
}
export function hasSharedSecretJwk(jwk) {
return 'k' in jwk && jwk.k != null;
}
export function hasPrivateSecretJwk(jwk) {
return 'd' in jwk && jwk.d != null;
}
export function isPrivateJwk(jwk) {
return hasPrivateSecretJwk(jwk) || hasSharedSecretJwk(jwk);
}
export function isPublicJwk(jwk) {
return !hasPrivateSecretJwk(jwk) && !hasSharedSecretJwk(jwk);
}
//# sourceMappingURL=jwk.js.map