Skip to content

Commit

Permalink
feat: support renewing certificates
Browse files Browse the repository at this point in the history
  • Loading branch information
hyrsky committed Jun 16, 2019
1 parent 2e34ed3 commit f66c493
Show file tree
Hide file tree
Showing 5 changed files with 196 additions and 10 deletions.
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,28 @@ Install with [npm](https://www.npmjs.com/):
## Usage

Autogenerated docs at: https://hyrsky.github.io/pankkiyhteys

### Renewing certificate

```js
function isAboutToExpire(key) {
const dateToCheck = new Date()
dateToCheck.setMonth(dateToCheck.getMonth() + 2)
return key.expires() < dateToCheck
}

const key = new Key(oldPrivateKey, oldCert)
const client = new Osuuspankki('1234567890', key, 'FI')

if (isAboutToExpire(key)) {
/**
* You have to:
* * generate new key.
* * save key to persistent storage before renewal.
*/
const keys = await Key.generateKey()
await writeFile('./newkey.pem', keys.privateKey)
const certificate = await client.getCertificate(keys.privateKey)
await writeFile('./newcert.pem', certificate)
}
```
4 changes: 2 additions & 2 deletions src/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ export class Client extends SoapClient {
*
* @param xml
*/
private signApplicationRequest(xml: string) {
protected signApplicationRequest(xml: string) {
return sign(xml, this.key, [], {
canonicalizationAlgorithm: 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315'
})
Expand All @@ -275,7 +275,7 @@ export class Client extends SoapClient {
/**
* Verify request signature in application request parsing callback
*/
private verifyRequestCallback: ParsePreprocess = async (xml, document) => {
protected verifyRequestCallback: ParsePreprocess = async (xml, document) => {
await verifyApplicationRequestSignature(xml, document, this.trustStore)
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/node-forge.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,8 @@ declare module 'node-forge' {

function certificationRequestToPem(cert: Certificate, maxline?: number): PEM

function certificationRequestToAsn1(cert: Certificate): any

function certificationRequestFromPem(
pem: PEM,
computeHash?: boolean,
Expand Down
85 changes: 79 additions & 6 deletions src/pankkiyhteys.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* @file Main entrypoint for library
* @file Main entrypoint
*/

import * as builder from 'xmlbuilder'
Expand All @@ -8,12 +8,13 @@ import { v4 as uuid } from 'uuid'
import createDebug from 'debug'

import SoapClient from './soap'
import TrustStore, { sign, Key } from './trust'
import TrustStore, { generateSigningRequest, sign, Key } from './trust'
import { X509ToCertificate } from './xml'
import * as app from './application'

// Certificates
// Certificate authority
import { OPPohjola, OPPohjolaTest } from './cacerts/OP-Pohjola'
import { pki } from 'node-forge'

const debug = createDebug('pankkiyhteys')

Expand Down Expand Up @@ -45,6 +46,10 @@ interface CertApplicationRequest {
SoftwareId: string
/** Service indentifier */
Service: string
/** pkcs#10 request */
Content?: string
/** Shared secret */
TransferKey?: string
}

export { Key } from './trust'
Expand All @@ -60,7 +65,7 @@ export class OsuuspankkiCertService extends SoapClient implements app.CertServic
this.environment = environment
}

private static getEndpoint(environment: app.Environment) {
static getEndpoint(environment: app.Environment) {
return {
[app.Environment.PRODUCTION]: 'https://wsk.op.fi/services/OPCertificateService',
[app.Environment.TEST]: 'https://wsk.asiakastesti.op.fi/services/OPCertificateService'
Expand Down Expand Up @@ -110,8 +115,6 @@ export class OsuuspankkiCertService extends SoapClient implements app.CertServic
}
)

const header = app.parseResponseHeader(response)

// Use preprocess callback that adds certificates to trust store.
// Otherwise we might not have intermediary certificates before signature validation.
await app.parseApplicationResponse(response, async (xml, document) => {
Expand Down Expand Up @@ -166,4 +169,74 @@ export class Osuuspankki extends app.Client {
[app.Environment.TEST]: 'https://wsk.asiakastesti.op.fi/services/CorporateFileService'
}[environment]
}

/**
* Get new certificate from cert service.
*
* Private must have following conditions:
* * Modulus lenth = 2048
* * If key already has signed certificate the current certificate will be returned instead.
*
* Client must save the private key to persistent storage before calling this method.
*
* @todo: replace currently used key
*
* @param newPrivateKey RSA private key (pem)
*/
async getCertificate(privateKey: string, transferKey: string) {
debug('renewCertificate')

const csr = generateSigningRequest(privateKey, this.username, 'FI')

const request: CertApplicationRequest = {
'@xmlns': 'http://op.fi/mlp/xmldata/',
CustomerId: this.username,
Timestamp: this.formatTime(new Date()),
Environment: this.environment,
SoftwareId: app.VERSION_STRING,
Service: 'MATU',
Content: csr
}

// Convert application request xml.
const requestXml = this.signApplicationRequest(
builder
.create({ CertApplicationRequest: request }, { version: '1.0', encoding: 'UTF-8' })
.end()
)

// Request id cannot be longer that 35 characters.
const requestId = uuid().substr(0, 35)

// Cert service envelopes are not signed.
const response = await this.makeSoapRequest(
OsuuspankkiCertService.getEndpoint(this.environment),
{
getCertificatein: {
'@xmlns': 'http://mlp.op.fi/OPCertificateService',
RequestHeader: {
SenderId: this.username,
RequestId: requestId,
Timestamp: this.formatTime(new Date())
},
ApplicationRequest: Buffer.from(requestXml).toString('base64')
}
}
)

const applicationResponse = await app.parseApplicationResponse(
response,
this.verifyRequestCallback
)

const {
CertApplicationResponse: {
Certificates: {
Certificate: { Name, Certificate, CertificateFormat }
}
}
} = applicationResponse

return pki.certificateToPem(X509ToCertificate(Certificate))
}
}
90 changes: 88 additions & 2 deletions src/trust.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as os from 'os'
import * as path from 'path'
import { generateKeyPair } from 'crypto'
import * as xpath from 'xpath'
import createDebug from 'debug'

Expand Down Expand Up @@ -27,6 +28,53 @@ export class Key {
constructor(key: string, cert: string) {
this.privateKey = key
this.certificate = pki.certificateFromPem(cert)

const dateToCheck = new Date()
dateToCheck.setMonth(dateToCheck.getMonth() + 1)
if (this.expires() < dateToCheck) {
debug('warning: certificate is about to expire')
}
}

/**
* Generate new key.
*
* @param commonName WS username.
* @param country Two letter country code.
*/
static async generateKey(commonName: string, country: string) {
debug('Generating 2048-bit key-pair...')
return new Promise<{ publicKey: string; privateKey: string }>((resolve, reject) =>
// node-forge also offers key generation function but frankly I trust nodejs crypto more.
generateKeyPair(
// @ts-ignore
'rsa',
{
modulusLength: 2048,
publicKeyEncoding: {
type: 'spki',
format: 'pem'
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'pem'
}
},
(err: Error | null, pemPublicKey: string, pemPrivateKey: string) => {
if (err) {
return reject(err)
}
resolve({
publicKey: pemPublicKey,
privateKey: pemPrivateKey
})
}
)
)
}

expires(): Date {
return this.certificate.validity.notAfter
}

getBase64Certificate() {
Expand All @@ -47,6 +95,46 @@ interface SignExtraOptions {

type SignOptions = ComputeSignatureOptions & SignExtraOptions

/**
* Convert certificate signing request to base64 encoded der
*
* @param csr
*/
function encodeSigningRequest(csr: pki.Certificate) {
return util.encode64(asn1.toDer(pki.certificationRequestToAsn1(csr)).getBytes())
}

/**
* Create certificate signing request from pem encoded private key.
*
* @return base der formatted csr.
*/
export function generateSigningRequest(
privateKeyPem: string,
commonName: string,
countryName: string
) {
const privateKey = pki.privateKeyFromPem(privateKeyPem)
const publicKey = pki.rsa.setPublicKey((privateKey as any).n, (privateKey as any).e)

const csr = pki.createCertificationRequest()
csr.publicKey = publicKey
csr.setSubject([
{
name: 'commonName',
value: commonName
},
{
name: 'countryName',
value: countryName
}
])
csr.sign(privateKey)

debug('Certification request (CSR) created.')
return encodeSigningRequest(csr)
}

/**
* Sign xml document
*
Expand Down Expand Up @@ -184,8 +272,6 @@ export default class TrustStore {
/**
* Test if given certificate is trusted.
*
* @todo: Support certificate chains.
*
* @param certificate Certificate to test.
* @param noLoading Don't attempt to load new intermediary certificates.
*/
Expand Down

0 comments on commit f66c493

Please sign in to comment.