UNPKG

undersign

Version:

Create eIDAS compatible XAdES digital signatures with certificate OCSP responses and timestamps. Works with the Estonian Id-card and Mobile-Id to create BDOCs, but isn't limited to Estonia.

354 lines (299 loc) 11.2 kB
var _ = require("../../lib/underscore") var Url = require("url") var Crypto = require("crypto") var MobileId = require("../../lib/mobile_id") var MobileIdError = require("../../lib/mobile_id").MobileIdError var Certificate = require("../../lib/certificate") var newCertificate = require("../fixtures").newCertificate var wait = require("../../lib/promise").wait var parseJson = require("../mitm").parseJson var jsonify = _.compose(JSON.parse, JSON.stringify) var PHONE_NUMBER = "+3725031337" var ID_NUMBER = "38706181337" var PARTY_NAME = "example.com" var PARTY_UUID = "e7fd0962-6454-4333-a773-144a3aaa7f08" var SESSION_ID = "f09c12d9-7e4c-4f9a-a7a7-bcffff1cd61c" var CERTIFICATE = new Certificate(newCertificate()) var mobileId = new MobileId("http://example.com/mid/", { user: PARTY_NAME, password: PARTY_UUID }) var MOBILE_ID_ERRORS = [ "TIMEOUT", "NOT_MID_CLIENT", "USER_CANCELLED", "SIGNATURE_HASH_MISMATCH", "PHONE_ABSENT", "DELIVERY_ERROR", "SIM_ERROR" ] describe("MobileId", function() { require("../mitm")() describe(".prototype.readCertificate", function() { it("must resolve with a certificate", function*() { var cert = mobileId.readCertificate(PHONE_NUMBER, ID_NUMBER) var req = yield wait(this.mitm, "request") req.method.must.equal("POST") req.headers.host.must.equal("example.com") req.headers["content-type"].must.equal("application/json") req.headers.accept.must.equal("application/json") req.url.must.equal("/mid/certificate") var body = yield parseJson(req) body.relyingPartyName.must.equal(PARTY_NAME) body.relyingPartyUUID.must.equal(PARTY_UUID) body.phoneNumber.must.equal(PHONE_NUMBER) body.nationalIdentityNumber.must.equal(ID_NUMBER) respond({result: "OK", cert: CERTIFICATE.toString("base64")}, req) cert = yield cert cert.must.be.an.instanceof(Certificate) cert.serialNumber.must.eql(CERTIFICATE.serialNumber) }) ;["NOT_FOUND", "NOT_ACTIVE"].forEach(function(code) { it(`must reject with MobileIdError given ${code} error`, function*() { var cert = mobileId.readCertificate(PHONE_NUMBER, ID_NUMBER) var req = yield wait(this.mitm, "request") respond({result: code}, req) var err try { yield cert } catch (ex) { err = ex } err.must.be.an.error(MobileIdError) err.code.must.equal(code) err.response.must.exist() }) }) it("must reject with MobileIdError given 400 Bad Request", function*() { var cert = mobileId.readCertificate(PHONE_NUMBER, "387061869") var req = yield wait(this.mitm, "request") req.res.statusCode = 400 respond({error: "nationalIdentityNumber must contain of 11 digits"}, req) var err try { yield cert } catch (ex) { err = ex } err.must.be.an.error(MobileIdError) err.code.must.equal("BAD_REQUEST") err.message.must.equal("nationalIdentityNumber must contain of 11 digits") err.response.must.exist() }) it("must reject with MobileIdError given 401 Unauthorized", function*() { var cert = mobileId.readCertificate(PHONE_NUMBER, "387061869") var req = yield wait(this.mitm, "request") req.res.statusCode = 401 respond({error: "Failed to authorize user"}, req) var err try { yield cert } catch (ex) { err = ex } err.must.be.an.error(MobileIdError) err.code.must.equal("UNAUTHORIZED") err.message.must.equal("Failed to authorize user") err.response.must.exist() }) }) function mustHandleErrors(request, waitSession) { describe("as a Mobile-Id method", function() { it("must resolve with null if session still running", function*() { var session = request() var req = yield wait(this.mitm, "request") respond({sessionID: SESSION_ID}, req) var res = waitSession(session) req = yield wait(this.mitm, "request") respond({state: "RUNNING"}, req) yield res.must.then.be.null() }) it("must reject with MobileIdError given 400 Bad Request", function*() { var session = request({language: "FOO"}) var req = yield wait(this.mitm, "request") req.res.statusCode = 400 respond({error: "Unknown language 'FOO'"}, req) var err try { yield session } catch (ex) { err = ex } err.must.be.an.error(MobileIdError) err.code.must.equal("BAD_REQUEST") err.message.must.equal("Unknown language 'FOO'") err.response.must.exist() }) it("must reject with MobileIdError given 401 Unauthorized", function*() { var session = request() var req = yield wait(this.mitm, "request") req.res.statusCode = 401 respond({error: "Failed to authorize user"}, req) var err try { yield session } catch (ex) { err = ex } err.must.be.an.error(MobileIdError) err.code.must.equal("UNAUTHORIZED") err.message.must.equal("Failed to authorize user") err.response.must.exist() }) it("must reject with MobileIdError given 400 Bad Request to session", function*() { var session = request() var req = yield wait(this.mitm, "request") respond({sessionID: SESSION_ID}, req) var res = waitSession(session) req = yield wait(this.mitm, "request") req.res.statusCode = 400 respond({error: "Invalid format for sessionId='foo'"}, req) var err try { yield res } catch (ex) { err = ex } err.must.be.an.error(MobileIdError) err.code.must.equal("BAD_REQUEST") err.message.must.equal("Invalid format for sessionId='foo'") err.response.must.exist() }) it("must reject with MobileIdError given 401 Unauthorized to session", function*() { var session = request() var req = yield wait(this.mitm, "request") respond({sessionID: SESSION_ID}, req) var res = waitSession(session) req = yield wait(this.mitm, "request") req.res.statusCode = 401 respond({error: "Failed to authorize user"}, req) var err try { yield res } catch (ex) { err = ex } err.must.be.an.error(MobileIdError) err.code.must.equal("UNAUTHORIZED") err.message.must.equal("Failed to authorize user") err.response.must.exist() }) MOBILE_ID_ERRORS.forEach(function(code) { it(`must reject with MobileIdError given ${code} error to session`, function*() { var session = request() var req = yield wait(this.mitm, "request") respond({sessionID: SESSION_ID}, req) var res = waitSession(session) req = yield wait(this.mitm, "request") respond({state: "COMPLETE", result: code}, req) var err try { yield res } catch (ex) { err = ex } err.must.be.an.error(MobileIdError) err.code.must.equal(code) err.message.must.not.equal(code) err.response.must.exist() }) }) }) } describe(".prototype.authenticate", function() { mustHandleErrors(mobileId.authenticate.bind( mobileId, PHONE_NUMBER, ID_NUMBER, Crypto.randomBytes(32) ), mobileId.waitForAuthentication.bind(mobileId)) it("must resolve with a session that resolves with an authentication", function*() { var authableHash = Crypto.randomBytes(32) var session = mobileId.authenticate( PHONE_NUMBER, ID_NUMBER, authableHash ) var req = yield wait(this.mitm, "request") req.method.must.equal("POST") req.headers.host.must.equal("example.com") req.headers["content-type"].must.equal("application/json") req.headers.accept.must.equal("application/json") req.url.must.equal("/mid/authentication") var body = yield parseJson(req) body.relyingPartyName.must.equal(PARTY_NAME) body.relyingPartyUUID.must.equal(PARTY_UUID) body.phoneNumber.must.equal(PHONE_NUMBER) body.nationalIdentityNumber.must.equal(ID_NUMBER) body.hash.must.equal(authableHash.toString("base64")) body.hashType.must.equal("SHA256") body.language.must.equal("EST") respond({sessionID: SESSION_ID}, req) var res = mobileId.waitForAuthentication(yield session, 10) req = yield wait(this.mitm, "request") req.method.must.equal("GET") req.headers.host.must.equal("example.com") req.headers.accept.must.equal("application/json") var url = Url.parse(req.url, true) url.pathname.must.equal("/mid/authentication/session/" + SESSION_ID) url.query.timeoutMs.must.equal("10000") respond({ result: "OK", state: "COMPLETE", cert: CERTIFICATE.toString("base64"), signature: { algorithm: "sha256WithRSAEncryption", value: Buffer.from("coffee").toString("base64") } }, req) var certAndSignature = yield res certAndSignature[0].must.be.an.instanceof(Certificate) certAndSignature[0].serialNumber.must.eql(CERTIFICATE.serialNumber) certAndSignature[1].must.eql(Buffer.from("coffee")) }) }) describe(".prototype.sign", function() { mustHandleErrors(mobileId.sign.bind( mobileId, PHONE_NUMBER, ID_NUMBER, Crypto.randomBytes(32) ), mobileId.waitForSignature.bind(mobileId)) it("must resolve with a session that resolves with a signature", function*() { var signableHash = Crypto.randomBytes(32) var session = mobileId.sign(PHONE_NUMBER, ID_NUMBER, signableHash) var req = yield wait(this.mitm, "request") req.method.must.equal("POST") req.headers.host.must.equal("example.com") req.headers["content-type"].must.equal("application/json") req.headers.accept.must.equal("application/json") req.url.must.equal("/mid/signature") var body = yield parseJson(req) body.relyingPartyName.must.equal(PARTY_NAME) body.relyingPartyUUID.must.equal(PARTY_UUID) body.phoneNumber.must.equal(PHONE_NUMBER) body.nationalIdentityNumber.must.equal(ID_NUMBER) body.hash.must.equal(signableHash.toString("base64")) body.hashType.must.equal("SHA256") body.language.must.equal("EST") respond({sessionID: SESSION_ID}, req) var signature = mobileId.waitForSignature(yield session, 10) req = yield wait(this.mitm, "request") req.method.must.equal("GET") req.headers.host.must.equal("example.com") req.headers.accept.must.equal("application/json") var url = Url.parse(req.url, true) url.pathname.must.equal("/mid/signature/session/" + SESSION_ID) url.query.timeoutMs.must.equal("10000") respond({ result: "OK", state: "COMPLETE", signature: { algorithm: "sha256WithRSAEncryption", value: Buffer.from("coffee").toString("base64") } }, req) yield signature.must.then.eql(Buffer.from("coffee")) }) }) describe(".confirmation", function() { it("must calculate confirmation from hash", function() { MobileId.confirmation(Buffer.from( "78225701eab0c124a4909e28a7e3323f48c9e0de828f690763bcc57ac06de0e1", "hex" )).must.equal(3937) }) }) describe("MobileIdError", function() { describe(".prototype.toJSON", function() { // This will be handy when storing the error in the database for // asynchronous status querying. it("must serialize with name, code and message", function() { var msg = "Person did not respond in time" var err = new MobileIdError("TIMEOUT", msg, {response: {body: "foo"}}) jsonify(err).must.eql({ name: "MobileIdError", code: "TIMEOUT", message: msg }) }) }) }) }) function respond(obj, req) { req.res.setHeader("Content-Type", "application/json") req.res.end(JSON.stringify(obj)) }