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.
404 lines (333 loc) • 13.1 kB
JavaScript
var Url = require("url")
var Crypto = require("crypto")
var SmartId = require("../../lib/smart_id")
var SmartIdError = require("../../lib/smart_id").SmartIdError
var SmartIdCertificate = require("../../lib/smart_id").SmartIdCertificate
var Certificate = require("../../lib/certificate")
var newCertificate = require("../fixtures").newCertificate
var wait = require("../../lib/promise").wait
var parseJson = require("../mitm").parseJson
var PARTY_NAME = "example.com"
var PARTY_UUID = "e7fd0962-6454-4333-a773-144a3aaa7f08"
var ID_NUMBER = "38706181337"
var CERTIFICATE = new Certificate(newCertificate())
var DOCUMENT_NUMBER = "PNOEE-" + ID_NUMBER + "-R2D2-Q"
var SESSION_ID = "f09c12d9-7e4c-4f9a-a7a7-bcffff1cd61c"
var smartId = new SmartId("http://example.com/smartid/", {
user: PARTY_NAME,
password: PARTY_UUID
})
var SMART_ID_ERRORS = [
"TIMEOUT",
"USER_REFUSED",
"DOCUMENT_UNUSABLE",
"WRONG_VC"
]
describe("SmartId", function() {
require("../mitm")()
function mustHandleErrors(request) {
describe("as a Smart-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 = smartId.wait(yield session)
req = yield wait(this.mitm, "request")
respond({state: "RUNNING"}, req)
yield res.must.then.be.null()
})
it("must reject with SmartIdError given 400 Bad Request", function*() {
var session = request({certificateLevel: "FOOBAR"})
var req = yield wait(this.mitm, "request")
req.res.statusCode = 400
respond({code: 400, message: "Bad Request"}, req)
var err
try { yield session } catch (ex) { err = ex }
err.must.be.an.error(SmartIdError)
err.code.must.equal("BAD_REQUEST")
err.message.must.equal("Bad Request")
err.response.must.exist()
})
it("must reject with SmartIdError given 401 Unauthorized", function*() {
var session = request()
var req = yield wait(this.mitm, "request")
req.res.statusCode = 401
respond({code: 401, message: "Unauthorized"}, req)
var err
try { yield session } catch (ex) { err = ex }
err.must.be.an.error(SmartIdError)
err.code.must.equal("UNAUTHORIZED")
err.message.must.equal("Unauthorized")
err.response.must.exist()
})
it("must reject with SmartIdError given 403 Forbidden", function*() {
var session = request({certificateLevel: "ADVANCED"})
var req = yield wait(this.mitm, "request")
req.res.statusCode = 403
respond({code: 403, message: "Forbidden"}, req)
var err
try { yield session } catch (ex) { err = ex }
err.must.be.an.error(SmartIdError)
err.code.must.equal("FORBIDDEN")
err.message.must.equal("Forbidden")
err.response.must.exist()
})
it("must reject with SmartIdError given 404 Not Found", function*() {
var session = request()
var req = yield wait(this.mitm, "request")
req.res.statusCode = 404
respond({code: 404, message: "Not Found"}, req)
var err
try { yield session } catch (ex) { err = ex }
err.must.be.an.error(SmartIdError)
err.code.must.equal("ACCOUNT_NOT_FOUND")
err.message.must.equal("Not Found")
err.response.must.exist()
})
it("must reject with SmartIdError given 471 No Suitable Account",
function*() {
var session = request()
var req = yield wait(this.mitm, "request")
req.res.statusCode = 471
respond({code: 471, message: "No suitable account found"}, req)
var err
try { yield session } catch (ex) { err = ex }
err.must.be.an.error(SmartIdError)
err.code.must.equal("NO_SUITABLE_CERTIFICATE")
err.message.must.equal("No suitable account found")
err.response.must.exist()
})
it("must reject with SmartIdError given 580 System Under Maintenance",
function*() {
var session = request()
var req = yield wait(this.mitm, "request")
req.res.statusCode = 580
respond({code: 580, message: "System is under maintenance"}, req)
var err
try { yield session } catch (ex) { err = ex }
err.must.be.an.error(SmartIdError)
err.code.must.equal("MAINTENANCE")
err.message.must.equal("System is under maintenance")
err.response.must.exist()
})
SMART_ID_ERRORS.forEach(function(code) {
it(`must reject with SmartIdError given ${code} error to session`,
function*() {
var session = request()
var req = yield wait(this.mitm, "request")
respond({sessionID: SESSION_ID}, req)
var res = smartId.wait(yield session)
req = yield wait(this.mitm, "request")
respond({state: "COMPLETE", result: {endResult: code}}, req)
var err
try { yield res } catch (ex) { err = ex }
err.must.be.an.error(SmartIdError)
err.code.must.equal(code)
err.message.must.not.equal(code)
err.response.must.exist()
})
})
})
}
describe(".prototype.certificate", function() {
mustHandleErrors(smartId.certificate.bind(smartId, "PNOEE-" + ID_NUMBER))
it("must resolve with a session that resolves with a certificate",
function*() {
var session = smartId.certificate("PNOEE-" + 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("/smartid/certificatechoice/etsi/PNOEE-" + ID_NUMBER)
var body = yield parseJson(req)
body.relyingPartyName.must.equal(PARTY_NAME)
body.relyingPartyUUID.must.equal(PARTY_UUID)
respond({sessionID: SESSION_ID}, req)
var cert = smartId.wait(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("/smartid/session/" + SESSION_ID)
url.query.timeoutMs.must.equal("10000")
respond({
state: "COMPLETE",
result: {endResult: "OK", documentNumber: DOCUMENT_NUMBER},
cert: {
value: CERTIFICATE.toString("base64"),
certificateLevel: "QUALIFIED"
}
}, req)
cert = yield cert
cert.must.be.an.instanceof(SmartIdCertificate)
cert.serialNumber.must.eql(CERTIFICATE.serialNumber)
cert.smartId.must.equal(DOCUMENT_NUMBER)
cert.level.must.equal("QUALIFIED")
})
it("must query via document id given SmartIdCertificate", function*() {
var cert = new SmartIdCertificate(newCertificate())
cert.smartId = DOCUMENT_NUMBER
smartId.certificate(cert)
var req = yield wait(this.mitm, "request")
req.method.must.equal("POST")
req.headers.host.must.equal("example.com")
req.url.must.equal("/smartid/certificatechoice/document/" + cert.smartId)
})
})
describe(".prototype.authenticate", function() {
mustHandleErrors(smartId.authenticate.bind(
smartId,
"PNOEE-" + ID_NUMBER,
Crypto.randomBytes(32)
))
it("must resolve with a session that resolves with a certificate",
function*() {
var authableHash = Crypto.randomBytes(32)
var session = smartId.authenticate("PNOEE-" + 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("/smartid/authentication/etsi/PNOEE-" + ID_NUMBER)
var body = yield parseJson(req)
body.relyingPartyName.must.equal(PARTY_NAME)
body.relyingPartyUUID.must.equal(PARTY_UUID)
body.hash.must.equal(authableHash.toString("base64"))
body.hashType.must.equal("SHA256")
respond({sessionID: SESSION_ID}, req)
var certAndSignature = smartId.wait(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("/smartid/session/" + SESSION_ID)
url.query.timeoutMs.must.equal("10000")
respond({
state: "COMPLETE",
result: {endResult: "OK", documentNumber: DOCUMENT_NUMBER},
cert: {
value: CERTIFICATE.toString("base64"),
certificateLevel: "QUALIFIED"
},
signature: {
algorithm: "sha256WithRSAEncryption",
value: Buffer.from("coffee").toString("base64")
}
}, req)
certAndSignature = yield certAndSignature
certAndSignature[0].must.be.an.instanceof(SmartIdCertificate)
certAndSignature[0].serialNumber.must.eql(CERTIFICATE.serialNumber)
certAndSignature[0].smartId.must.equal(DOCUMENT_NUMBER)
certAndSignature[0].level.must.equal("QUALIFIED")
certAndSignature[1].must.eql(Buffer.from("coffee"))
})
it("must query via document id given SmartIdCertificate", function*() {
var cert = new SmartIdCertificate(newCertificate())
cert.smartId = DOCUMENT_NUMBER
var authableHash = Crypto.randomBytes(32)
smartId.authenticate(cert, authableHash)
var req = yield wait(this.mitm, "request")
req.method.must.equal("POST")
req.headers.host.must.equal("example.com")
req.url.must.equal("/smartid/authentication/document/" + cert.smartId)
var body = yield parseJson(req)
body.hash.must.equal(authableHash.toString("base64"))
})
})
describe(".prototype.sign", function() {
mustHandleErrors(smartId.sign.bind(
smartId,
"PNOEE-" + ID_NUMBER,
Crypto.randomBytes(32)
))
it("must resolve with a session that resolves with a signature",
function*() {
var signableHash = Crypto.randomBytes(32)
var session = smartId.sign("PNOEE-" + 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("/smartid/signature/etsi/PNOEE-" + ID_NUMBER)
var body = yield parseJson(req)
body.relyingPartyName.must.equal(PARTY_NAME)
body.relyingPartyUUID.must.equal(PARTY_UUID)
body.hash.must.equal(signableHash.toString("base64"))
body.hashType.must.equal("SHA256")
respond({sessionID: SESSION_ID}, req)
var certAndSignature = smartId.wait(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("/smartid/session/" + SESSION_ID)
url.query.timeoutMs.must.equal("10000")
respond({
state: "COMPLETE",
result: {endResult: "OK", documentNumber: DOCUMENT_NUMBER},
cert: {
value: CERTIFICATE.toString("base64"),
certificateLevel: "QUALIFIED"
},
signature: {
algorithm: "sha256WithRSAEncryption",
value: Buffer.from("coffee").toString("base64")
}
}, req)
certAndSignature = yield certAndSignature
certAndSignature[0].must.be.an.instanceof(SmartIdCertificate)
certAndSignature[0].serialNumber.must.eql(CERTIFICATE.serialNumber)
certAndSignature[0].smartId.must.equal(DOCUMENT_NUMBER)
certAndSignature[0].level.must.equal("QUALIFIED")
certAndSignature[1].must.eql(Buffer.from("coffee"))
})
it("must query via document id given SmartIdCertificate", function*() {
var cert = new SmartIdCertificate(newCertificate())
cert.smartId = DOCUMENT_NUMBER
var signableHash = Crypto.randomBytes(32)
smartId.sign(cert, signableHash)
var req = yield wait(this.mitm, "request")
req.method.must.equal("POST")
req.headers.host.must.equal("example.com")
req.url.must.equal("/smartid/signature/document/" + cert.smartId)
var body = yield parseJson(req)
body.hash.must.equal(signableHash.toString("base64"))
})
})
describe(".prototype.wait", function() {
it("must reject with SmartIdError if session not found", function*() {
var session = smartId.certificate("PNOEE-" + ID_NUMBER)
var req = yield wait(this.mitm, "request")
req.url.must.equal("/smartid/certificatechoice/etsi/PNOEE-" + ID_NUMBER)
respond({sessionID: SESSION_ID}, req)
var res = smartId.wait(yield session, 10)
req = yield wait(this.mitm, "request")
req.res.statusCode = 404
respond({code: 404, message: "Not Found"}, req)
var err
try { yield res } catch (ex) { err = ex }
err.must.be.an.error(SmartIdError)
err.code.must.equal("SESSION_NOT_FOUND")
err.message.must.equal("Not Found")
err.response.must.exist()
})
})
describe(".verification", function() {
it("must calculate verification code from hash", function() {
SmartId.verification(Buffer.from(
"78225701eab0c124a4909e28a7e3323f48c9e0de828f690763bcc57ac06de0e1",
"hex"
)).must.equal(2931)
})
})
})
function respond(obj, req) {
req.res.setHeader("Content-Type", "application/json")
req.res.end(JSON.stringify(obj))
}