Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: adds fromPem method for identity-secp256k1 #816

Merged
merged 11 commits into from
Feb 21, 2024
1 change: 1 addition & 0 deletions docs/generated/changelog.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ <h1>Agent-JS Changelog</h1>
<section>
<h2>Version x.x.x</h2>
<ul>
<li>feat: adds `fromPem` method for `identity-secp256k1`</li>
<li>feat: replaces `secp256k1` npm package with `@noble/curves`</li>
<li>feat: enhances `.from` methods on public key classes to support unknown types, including PublicKey instances, ArrayBuffer-like objects, DER encoded public keys, and hex strings. Also introduces a new `bufFromBufLike` util</li>
<li>feat: introduces partial identities from public keys for authentication flows</li>
Expand Down
77 changes: 77 additions & 0 deletions packages/identity-secp256k1/src/pem.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { pemToSecretKey } from './pem';
import { Secp256k1KeyIdentity } from './secp256k1';

const pem = `-----BEGIN EC PRIVATE KEY-----
MHQCAQEEIGfKHuyoCCCbEXb0789MIdWiCIpZo1LaKApv95SSIaWPoAcGBSuBBAAK
oUQDQgAEahC99Avid7r8D6kIeLjjxJ8kwdJRy5nPrN9o18P7xHT95i0JPr5ivc9v
CB8vG2s97NB0re2MhqvdWgradJZ8Ow==
-----END EC PRIVATE KEY-----
`;
describe('pemToSecretKey', () => {
it('should parse a PEM-encoded key', () => {
const key = pemToSecretKey(pem);
expect(key).toBeDefined();
});
it('should resolve an expected principal', () => {
const expected = '42gbo-uiwfn-oq452-ql6yp-4jsqn-a6bxk-n7l4z-ni7os-yptq6-3htob-vqe';

const key = pemToSecretKey(pem);
const identity = Secp256k1KeyIdentity.fromSecretKey(key);
const principal = identity.getPrincipal();
expect(principal.toString()).toEqual(expected);
});
it('should throw errors for invalid PEMs', () => {
const invalidPems = [
// no header
`EC PRIVATE KEY-----
MHQCAQEEIGfKHuyoCCCbEXb0789MIdWiCIpZo1LaKApv95SSIaWPoAcGBSuBBAAK
oUQDQgAEahC99Avid7r8D6kIeLjjxJ8kwdJRy5nPrN9o18P7xHT95i0JPr5ivc9v
CB8vG2s97NB0re2MhqvdWgradJZ8Ow==
`,
// no footer
`-----BEGIN EC PRIVATE KEY-----
MHQCAQEEIGfKHuyoCCCbEXb0789MIdWiCIpZo1LaKApv95SSIaWPoAcGBSuBBAAK
oUQDQgAEahC99Avid7r8D6kIeLjjxJ8kwdJRy5nPrN9o18P7xHT95i0JPr5ivc9v
CB8vG2s97NB0re2MhqvdWgradJZ8Ow==`,
// no body
`-----BEGIN EC PRIVATE KEY-----
-----END PRIVATE KEY-----`,
// no newlines
`-----BEGIN EC PRIVATE KEY-----MHQCAQEEIGfKHuyoCCCbEXb0789MIdWiCIpZo1LaKApv95SSIaWPoAcGBSuBBAAKoUQDQgAEahC99Avid7r8D6kIeLjjxJ8kwdJRy5nPrN9o18P7xHT95i0JPr5ivc9vCB8vG2s97NB0re2MhqvdWgradJZ8Ow==-----END EC PRIVATE KEY-----`,

// too long
`-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCvQz0EmoKHRxj4
oAD/n+u2GXjxTH4DrXBaUkoZ48BTXon53M05xh1Nq9rJ/efT8q9W8d8cfgKp+GAn
DcpJxHt35vXuB/rCVeZOhnSYiKmqvlWH7qTEoGdwNjPpQ7/RqRpzn168xCiU2lP1
6YGF4ThNi+FH5OxC5CDv2Ms46A+Y1uc5qLJH0RZO9hWs93WhN9el7VENF9D/XG/U
Mwy4cUc+qn1ER+HcpmjAFAnGxtnVitLAsNVxv08PWCX5c7rF+b1hZ4cCbxeWBZAu
khPvsK/r75NDign7Fp8zJuRdfgFy2PnMw5S2fuj+VcLPPYAjBgMzU1C7hSCq48JF
cAP2Ryx7AgMBAAECggEAL2YDzobq3iMAQd0j5/4cBTeGWdvSCLSTOhofKDlL/kAH
GKf6aLGHo0Xi+dXNKKjtepoXOOFrXwRpHKbCGokkyxyPTjyiOIR6sKn0RnxPRnoL
L6P+s56d2t8N0vwbmFwfZz2mpW53eypAorTv7oEmdPJrjsH+k2iW78a1z0ITVcX2
VkSXMFuDCjoD9cqgO3SIjw2ZA3q+C78Sqk3qije8/oC3SJZckt6RlHgh0FBAiJ0Z
lAjQ7KK+bNupEjNK1YLDzQxkcFLol8EpF1kmxtsbnRMurnRnJqWfrJO4Xn/2uB8Z
zNcCBLj4ZTyOHciLuvSGDbhQ4EMhHkWr2JTXRYuRoQKBgQDWbymEzs+RiShwC/b0
MiQpPLmjj5UunZNLKK8xWhEhqKZ06tmCJT/kY8sEDAX6DVBGMYK8A7KNLzh4BC4h
KC/C7UjWD7xUWW8nEplVnslqIfpOsvbM7+lRLFsmT7eu4MRheVmO+lOQxoBM5nqV
gDcLJu4kclKS9/MRcSh2in6ClQKBgQDRPEVS9G59lV9wXQWAteXO1Byf5cwaDiti
H1hXHspZJYDqdgGCa9fsI0tqe3XP/0O0Hl6AMLKkaoWfxsIIL4gYnV5HnUJDDcPA
ZkuemPqmNlxmOUprRvZxaSMkV0Jntweu+XJ5WKNTnzyn2cJf7QueLR6I4/nbRS+E
40qARY2+zwKBgEUwVPsvJ7ZTxSJyGdqtGxHbMCLgP0htO4tysyR/ZSuxGRR8enYN
wtHUiTrjDkKibRZY/0/e+YuogtXms2Orbc29dlTret7UhJLc43DG7UI7eGJQSGXT
uzqfz0FLU38vsu2olAcYKkJ6agdmDoOSfTAx/YDxCke1jU5Bbsbg5PUJAoGBAJW0
pPlMsL2kIaw4slY8T5gjxfNWLSm7V6kWOlPjUO5l2g5nrn7NgKmRO0WN3mabAqse
S4k2zqq7GK6QPIY01BCgkDN3PlDRyWyhBJwOYtCH9qaheTC2jl/o1N8MnBOvLo0w
J4rRM9MCDRkfwmZ2KajcKYvSahRMNUrEgaqzmU6bAoGATJe1+dPiylfpOjPeLi+b
XUbrtdhkX98LWviI+xdFmM9aEIBNHRsXYlZ08jkZKg+tdDYbyYN1FulaeoBjO59Q
oKzGLP8ZY3TSs7tPHvYkMtJ8XAXdc+P1Pc9Gfb0LEUozLtBNJu/o0Ku0U6aiYSJH
AxFAcv7NorJ3jnKxFqeuL58=
-----END PRIVATE KEY-----
`,
];
invalidPems.forEach(pem => {
expect(() => pemToSecretKey(pem)).toThrow();
});
});
});
42 changes: 42 additions & 0 deletions packages/identity-secp256k1/src/pem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { concat, uint8ToBuf } from '@dfinity/agent';

const HEADER = `-----BEGIN`;
const FOOTER = `-----END`;
const ECHEADER = `-----BEGIN EC PRIVATE KEY`;

/**
* Parse a PEM-encoded key into an ArrayBuffer
* @param pem - the PEM-encoded key
* @returns secret key as an ArrayBuffer
*/
export function pemToSecretKey(pem: string): ArrayBuffer {
const lines = pem.trim().split('\n');
const header = lines[0].trim();
const footer = lines[lines.length - 1].trim();
if (lines.length < 3) {
throw new Error('Invalid PEM format');
}
if (!header.startsWith(HEADER)) {
throw new Error('Invalid PEM header');
}
if (!footer.startsWith(FOOTER)) {
throw new Error('Invalid PEM footer');
}
krpeacock marked this conversation as resolved.
Show resolved Hide resolved
const base64Data = lines.slice(1, -1).join('').replace(/\r?\n/g, '');
const rawKey = Uint8Array.from(atob(base64Data), c => c.charCodeAt(0));

if (pem.startsWith(ECHEADER)) {
if (rawKey.length !== 118) {
krpeacock marked this conversation as resolved.
Show resolved Hide resolved
throw new Error(`Invalid key length ${rawKey.length}. Expected 118 bytes.`);
} else {
return rawKey.slice(7, 39);
krpeacock marked this conversation as resolved.
Show resolved Hide resolved
}
}
if (rawKey.length != 85) {
throw new Error(`Invalid key length ${rawKey.length}. Expected 85 bytes.`);
} else {
return concat(rawKey.slice(16, 48), rawKey.slice(53, 85));
krpeacock marked this conversation as resolved.
Show resolved Hide resolved
}

return uint8ToBuf(rawKey);
}
12 changes: 12 additions & 0 deletions packages/identity-secp256k1/src/secp256k1.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,18 @@ describe('Secp256k1KeyIdentity Tests', () => {
});
});

test('fromPem should generate an identity', () => {
const pem = `-----BEGIN EC PRIVATE KEY-----
MHQCAQEEIGfKHuyoCCCbEXb0789MIdWiCIpZo1LaKApv95SSIaWPoAcGBSuBBAAK
oUQDQgAEahC99Avid7r8D6kIeLjjxJ8kwdJRy5nPrN9o18P7xHT95i0JPr5ivc9v
CB8vG2s97NB0re2MhqvdWgradJZ8Ow==
-----END EC PRIVATE KEY-----`;
const identity = Secp256k1KeyIdentity.fromPem(pem);
expect(identity.getPrincipal().toString()).toStrictEqual(
'42gbo-uiwfn-oq452-ql6yp-4jsqn-a6bxk-n7l4z-ni7os-yptq6-3htob-vqe',
);
});

describe('public key serialization from various types', () => {
it('should serialize from an existing public key', () => {
const baseKey = Secp256k1KeyIdentity.generate();
Expand Down
11 changes: 11 additions & 0 deletions packages/identity-secp256k1/src/secp256k1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import hdkey from 'hdkey';
import { mnemonicToSeedSync } from 'bip39';
import { PublicKey, SignIdentity } from '@dfinity/agent';
import { SECP256K1_OID, unwrapDER, wrapDER } from './der';
import { pemToSecretKey } from './pem';

declare type PublicKeyHex = string;
declare type SecretKeyHex = string;
Expand Down Expand Up @@ -202,6 +203,16 @@ export class Secp256k1KeyIdentity extends SignIdentity {
return Secp256k1KeyIdentity.fromSecretKey(addrnode.privateKey);
}

/**
* Utility method to create a Secp256k1KeyIdentity from a PEM-encoded key.
* @param pemKey - PEM-encoded key as a string
* @returns - Secp256k1KeyIdentity
*/
public static fromPem(pemKey: string): Secp256k1KeyIdentity {
const secretKey = pemToSecretKey(pemKey);
return this.fromSecretKey(secretKey);
}

_publicKey: Secp256k1PublicKey;

protected constructor(publicKey: Secp256k1PublicKey, protected _privateKey: ArrayBuffer) {
Expand Down
Loading