Skip to content

Commit

Permalink
fix issue#925
Browse files Browse the repository at this point in the history
  • Loading branch information
erossignon committed Jan 9, 2021
1 parent 6a57123 commit 0fb4ee3
Show file tree
Hide file tree
Showing 8 changed files with 112 additions and 49 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import * as mkdirp from "mkdirp";
import envPaths from "env-paths";
import { checkDebugFlag, make_debugLog, make_errorLog } from "node-opcua-debug";

import { Certificate, exploreCertificateInfo, makeSHA1Thumbprint, readCertificate, toPem } from "node-opcua-crypto";
import { Certificate, exploreCertificateInfo, makeSHA1Thumbprint, readCertificate, split_der, toPem } from "node-opcua-crypto";
import { CertificateManager, CertificateManagerOptions, CertificateStatus } from "node-opcua-pki";
import { StatusCodes } from "node-opcua-status-code";
import { StatusCode } from "node-opcua-status-code";
Expand Down Expand Up @@ -83,26 +83,28 @@ export class OPCUACertificateManager extends CertificateManager implements ICert
this.automaticallyAcceptUnknownCertificate = !!options.automaticallyAcceptUnknownCertificate;
}

public checkCertificate(certificate: Certificate): Promise<StatusCode>;
public checkCertificate(certificate: Certificate, callback: StatusCodeCallback): void;
public checkCertificate(certificate: Certificate, callback?: StatusCodeCallback): Promise<StatusCode> | void {
super.verifyCertificate(certificate, (err1?: Error | null, status?: string) => {
public checkCertificate(certificateChain: Certificate): Promise<StatusCode>;
public checkCertificate(certificateChain: Certificate, callback: StatusCodeCallback): void;
public checkCertificate(certificateChain: Certificate, callback?: StatusCodeCallback): Promise<StatusCode> | void {

super.verifyCertificate(certificateChain, (err1?: Error | null, status?: string) => {

if (err1) {
return callback!(err1);
}
const statusCode = (StatusCodes as any)[status!];

debugLog("checkCertificate => StatusCode = ", statusCode.toString());
if (statusCode === StatusCodes.BadCertificateUntrusted) {
const thumbprint = makeSHA1Thumbprint(certificate).toString("hex");
const thumbprint = makeSHA1Thumbprint(certificateChain).toString("hex");
if (this.automaticallyAcceptUnknownCertificate) {
debugLog("automaticallyAcceptUnknownCertificate = true");
debugLog("certificate with thumbprint " + thumbprint + " is now trusted");
return this.trustCertificate(certificate, () => callback!(null, StatusCodes.Good));
return this.trustCertificate(certificateChain, () => callback!(null, StatusCodes.Good));
} else {
debugLog("automaticallyAcceptUnknownCertificate = false");
debugLog("certificate with thumbprint " + thumbprint + " is now rejected");
return this.rejectCertificate(certificate, () => callback!(null, StatusCodes.BadCertificateUntrusted));
return this.rejectCertificate(certificateChain, () => callback!(null, StatusCodes.BadCertificateUntrusted));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ async function makeCertificateManager() {
if (g_certificateManager) {
return g_certificateManager;
}
const certificateManager = new OPCUACertificateManager({});
const certificateManager = new OPCUACertificateManager({
automaticallyAcceptUnknownCertificate: true
});
await certificateManager.initialize();

const issuerCertificateFile = path.join(certificate_store, "CA/public/cacert.pem");
Expand Down Expand Up @@ -89,9 +91,7 @@ function start_inner_server_local(options, callback) {
data.server = server;
callback(null, data);
});

});

}

function stop_inner_server_local(data, callback) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ import {
makeSHA1Thumbprint,
PrivateKeyPEM,
PublicKeyPEM,
rsa_length
rsa_length,
split_der
} from "node-opcua-crypto";

import { assert } from "node-opcua-assert";
Expand Down Expand Up @@ -69,7 +70,7 @@ const doTraceStatistics = process.env.NODEOPCUADEBUG && process.env.NODEOPCUADEB
const doPerfMonitoring = process.env.NODEOPCUADEBUG && process.env.NODEOPCUADEBUG.indexOf("PERF") >= 0;
const dumpSecurityHeader = process.env.NODEOPCUADEBUG && process.env.NODEOPCUADEBUG.indexOf("SECURITY") >= 0;

import { ICertificateKeyPairProvider, Request, Response } from "../common";
import { extractFirstCertificateInChain, getThumprint, ICertificateKeyPairProvider, Request, Response } from "../common";
import * as async from "async";

export const requestHandleNotSetValue = 0xdeadbeef;
Expand Down Expand Up @@ -338,7 +339,7 @@ export class ClientSecureChannelLayer extends EventEmitter {
constructor(options: ClientSecureChannelLayerOptions) {
super();

this.securityHeader = null;
this.securityHeader = null;
this.receiverCertificate = null;
this.securityToken = null;
this.serverNonce = null;
Expand Down Expand Up @@ -371,14 +372,15 @@ export class ClientSecureChannelLayer extends EventEmitter {

this.securityPolicy = coerceSecurityPolicy(options.securityPolicy);

this.serverCertificate = options.serverCertificate ? options.serverCertificate : null;
this.serverCertificate = extractFirstCertificateInChain(options.serverCertificate);

if (this.securityMode !== MessageSecurityMode.None) {
assert(
(this.serverCertificate as any) instanceof Buffer,
"Expecting a valid certificate when security mode is not None"
);
assert(this.securityPolicy !== SecurityPolicy.None, "Security Policy None is not a valid choice");
// make sure that we do not have a chain here ...
}

this.messageBuilder = new MessageBuilder({
Expand Down Expand Up @@ -437,6 +439,9 @@ export class ClientSecureChannelLayer extends EventEmitter {
public getCertificateChain(): Certificate | null {
return this.parent ? this.parent.getCertificateChain() : null;
}
public getCertificate(): Certificate | null {
return this.parent ? this.parent.getCertificate() : null;
}

public toString(): string {
let str = "";
Expand Down Expand Up @@ -1520,20 +1525,36 @@ export class ClientSecureChannelLayer extends EventEmitter {
}

private _construct_security_header() {
assert(this.hasOwnProperty("securityMode"));
assert(this.hasOwnProperty("securityPolicy"));
this.receiverCertificate = this.serverCertificate ? Buffer.from(this.serverCertificate) : null;

let securityHeader = null;
switch (this.securityMode) {
case MessageSecurityMode.Sign:
case MessageSecurityMode.SignAndEncrypt: {
assert(this.securityPolicy !== SecurityPolicy.None);
// get the thumbprint of the client certificate
const thumbprint = this.receiverCertificate ? makeSHA1Thumbprint(this.receiverCertificate) : null;
const receiverCertificateThumbprint = getThumprint(this.receiverCertificate);

securityHeader = new AsymmetricAlgorithmSecurityHeader({
receiverCertificateThumbprint: thumbprint, // thumbprint of the public key used to encrypt the message
receiverCertificateThumbprint, // thumbprint of the public key used to encrypt the message
securityPolicyUri: toURI(this.securityPolicy),

/**
* The X.509 v3 Certificate assigned to the sending application Instance.
* This is a DER encoded blob.
* The structure of an X.509 v3 Certificate is defined in X.509 v3.
* The DER format for a Certificate is defined in X690
* This indicates what Private Key was used to sign the MessageChunk.
* The Stack shall close the channel and report an error to the application if the SenderCertificate is too large for the buffer size supported by the transport layer.
* This field shall be null if the Message is not signed.
* If the Certificate is signed by a CA, the DER encoded CA Certificate may be
* appended after the Certificate in the byte array. If the CA Certificate is also
* signed by another CA this process is repeated until the entire Certificate chain
* is in the buffer or if MaxSenderCertificateSize limit is reached (the process
* stops after the last whole Certificate that can be added without exceeding
* the MaxSenderCertificateSize limit).
* Receivers can extract the Certificates from the byte array by using the Certificate
* size contained in DER header (see X.509 v3).
*/
senderCertificate: this.getCertificateChain() // certificate of the private key used to sign the message
});

Expand Down
15 changes: 15 additions & 0 deletions packages/node-opcua-secure-channel/source/common.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/**
* @module node-opcua-secure-channel
*/
import { makeSHA1Thumbprint, split_der } from "node-opcua-crypto";
import { TypeSchemaBase } from "node-opcua-factory";
import { CloseSecureChannelRequest, MessageSecurityMode, RequestHeader, ResponseHeader } from "node-opcua-service-secure-channel";
import { ServiceFault } from "./services";
Expand All @@ -21,3 +22,17 @@ export interface RequestB {
export type Request = RequestB | CloseSecureChannelRequest;

export { ICertificateKeyPairProvider } from "node-opcua-common";

export function extractFirstCertificateInChain(certificateChain?: Buffer | null): Buffer | null {
if (!certificateChain) {
return null;
}
const c = split_der(certificateChain);
return c[0];
}
export function getThumprint(certificateChain: Buffer|null): Buffer | null {
if (!certificateChain) {
return null;
}
return makeSHA1Thumbprint(extractFirstCertificateInChain(certificateChain)!);
}
4 changes: 3 additions & 1 deletion packages/node-opcua-secure-channel/source/message_chunker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,9 @@ export class MessageChunker {
options = options || {};
options.securityHeader =
options.securityHeader ||
new AsymmetricAlgorithmSecurityHeader({ securityPolicyUri: "http://opcfoundation.org/UA/SecurityPolicy#None" });
new AsymmetricAlgorithmSecurityHeader({
securityPolicyUri: "http://opcfoundation.org/UA/SecurityPolicy#None"
});

assert(options !== null && typeof options === "object");
assert(options.securityHeader !== null && typeof options.securityHeader === "object");
Expand Down
13 changes: 9 additions & 4 deletions packages/node-opcua-secure-channel/source/security_policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import {
computeDerivedKeys as computeDerivedKeys_ext,
DerivedKeys,
encryptBufferWithDerivedKeys,
exploreCertificate,
exploreCertificateInfo,
makeMessageChunkSignature,
makeMessageChunkSignatureWithDerivedKeys,
Expand All @@ -27,11 +26,11 @@ import {
RSA_PKCS1_OAEP_PADDING,
RSA_PKCS1_PADDING,
Signature,
split_der,
toPem,
verifyMessageChunkSignature,

} from "node-opcua-crypto";
import { SecureMessageChunkManagerOptions, VerifyBufferFunc } from "./secure_message_chunk_manager";
import { EncryptBufferFunc, SignBufferFunc } from "node-opcua-chunkmanager";

// tslint:disable:no-empty
Expand Down Expand Up @@ -439,12 +438,15 @@ export function computeSignature(
return undefined;
}

// Verify that senderCertifiate is not a chain
const chain = split_der(senderCertificate);

const cryptoFactory = getCryptoFactory(securityPolicy);
if (!cryptoFactory) {
return undefined;
}
// This parameter is calculated by appending the clientNonce to the clientCertificate
const dataToSign = Buffer.concat([senderCertificate, senderNonce]);
const dataToSign = Buffer.concat([chain[0], senderNonce]);

// ... and signing the resulting sequence of bytes.
const signature = cryptoFactory.asymmetricSign(dataToSign, receiverPrivateKey);
Expand Down Expand Up @@ -479,10 +481,13 @@ export function verifySignature(
// no signature provided
return false;
}
// Verify that senderCertifiate is not a chain
const chain = split_der(receiverCertificate);


assert(signature.signature instanceof Buffer);
// This parameter is calculated by appending the clientNonce to the clientCertificate
const dataToVerify = Buffer.concat([receiverCertificate, receiverNonce]);
const dataToVerify = Buffer.concat([chain[0], receiverNonce]);
return cryptoFactory.asymmetricVerify(dataToVerify, signature.signature, senderCertificate);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ import { Callback2, ErrorCallback } from "node-opcua-status-code";

import { SecureMessageChunkManagerOptions, SecurityHeader } from "../secure_message_chunk_manager";

import { ICertificateKeyPairProvider, Request, Response } from "../common";
import { getThumprint, ICertificateKeyPairProvider, Request, Response } from "../common";
import { MessageBuilder, ObjectFactory } from "../message_builder";
import { ChunkMessageOptions, MessageChunker } from "../message_chunker";
import {
Expand Down Expand Up @@ -528,8 +528,7 @@ export class ServerSecureChannelLayer extends EventEmitter {
}

public getSignatureLength(): PublicKeyLength {
const chain = this.getCertificateChain();
const firstCertificateInChain = split_der(chain)[0];
const firstCertificateInChain = this.getCertificate();
const cert = exploreCertificateInfo(firstCertificateInChain);
return cert.publicKeyLength; // 1024 bits = 128Bytes or 2048=256Bytes
}
Expand Down Expand Up @@ -635,7 +634,7 @@ export class ServerSecureChannelLayer extends EventEmitter {
const securityOptions = msgType === "OPN" ? this._get_security_options_for_OPN() : this._get_security_options_for_MSG();
if (securityOptions) {
options = {
...options,
...options,
...securityOptions
};
}
Expand Down Expand Up @@ -1036,18 +1035,32 @@ export class ServerSecureChannelLayer extends EventEmitter {
case MessageSecurityMode.Sign:
case MessageSecurityMode.SignAndEncrypt:
default: {
// get the thumbprint of the client certificate
const thumbprint = this.receiverCertificate ? makeSHA1Thumbprint(this.receiverCertificate) : null;
const receiverCertificateThumbprint = getThumprint(this.receiverCertificate);

if (!this.clientSecurityHeader) {
throw new Error("Internal");
}
const asymmClientSecurityHeader = this.clientSecurityHeader as AsymmetricAlgorithmSecurityHeader;

// istanbul ignore next
securityHeader = new AsymmetricAlgorithmSecurityHeader({
receiverCertificateThumbprint: thumbprint, // message not encrypted (????)
receiverCertificateThumbprint, // message not encrypted (????)
securityPolicyUri: asymmClientSecurityHeader.securityPolicyUri,
senderCertificate: this.getCertificateChain() // certificate of the private key used to sign the message
/**
* The X.509 v3 Certificate assigned to the sending application Instance.
* This is a DER encoded blob.
* The structure of an X.509 v3 Certificate is defined in X.509 v3.
* The DER format for a Certificate is defined in X690
* This indicates what Private Key was used to sign the MessageChunk.
* The Stack shall close the channel and report an error to the application if the SenderCertificate is too large for the buffer size supported by the transport layer.
* This field shall be null if the Message is not signed.
* If the Certificate is signed by a CA, the DER encoded CA Certificate may be
* appended after the Certificate in the byte array. If the CA Certificate is also
* signed by another CA this process is repeated until the entire Certificate chain
* is in the buffer or if MaxSenderCertificateSize limit is reached (the process
* stops after the last whole Certificate that can be added without exceeding
* the MaxSenderCertificateSize limit).
* Receivers can extract the Certificates from the byte array by using the Certificate
* size contained in DER header (see X.509 v3).
*/
senderCertificate: this.getCertificateChain() // certificate of the private key used to sign the message
});
}
}
Expand Down Expand Up @@ -1302,8 +1315,8 @@ export class ServerSecureChannelLayer extends EventEmitter {

if (clientSecurityHeader.receiverCertificateThumbprint) {
// check if the receiverCertificateThumbprint is my certificate thumbprint
const serverCertificateChain = this.getCertificateChain();
const myCertificateThumbPrint = makeSHA1Thumbprint(serverCertificateChain);
const serverCertificate = this.getCertificate();
const myCertificateThumbPrint = makeSHA1Thumbprint(serverCertificate);
const thisIsMyCertificate =
myCertificateThumbPrint.toString("hex") === clientSecurityHeader.receiverCertificateThumbprint.toString("hex");
if (doDebug && !thisIsMyCertificate) {
Expand Down
27 changes: 16 additions & 11 deletions packages/node-opcua-server/source/opcua_server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import {
import { OPCUACertificateManager } from "node-opcua-certificate-manager";
import { ServerState } from "node-opcua-common";
import { Certificate, exploreCertificate, Nonce, toPem } from "node-opcua-crypto";
import { AttributeIds, NodeClass } from "node-opcua-data-model";
import { AttributeIds, LocalizedText, NodeClass } from "node-opcua-data-model";
import { DataValue } from "node-opcua-data-value";
import { dump, make_debugLog, make_errorLog } from "node-opcua-debug";
import { NodeId } from "node-opcua-nodeid";
Expand Down Expand Up @@ -246,18 +246,23 @@ function getRequiredEndpointInfo(endpoint: EndpointDescription) {
securityLevel: endpoint.securityLevel,
securityMode: endpoint.securityMode,
securityPolicyUri: endpoint.securityPolicyUri,
server: { applicationUri: endpoint.server.applicationUri },
server: {
applicationUri: endpoint.server.applicationUri,
applicationType: endpoint.server.applicationType,
applicationName: endpoint.server.applicationName,
// ... to be continued after verifying what fields are actually needed
},
transportProfileUri: endpoint.transportProfileUri,
userIdentityTokens: endpoint.userIdentityTokens
});
// reduce even further by explicitly setting unwanted members to null
(e as any).productUri = null;
(e as any).applicationName = null;
(e as any).applicationType = null;
(e as any).gatewayServerUri = null;
(e as any).discoveryProfileUri = null;
(e as any).discoveryUrls = null;
(e as any).serverCertificate = null;
e.server.productUri = null;
e.server.applicationName = null as any;
//xxx e.server.applicationType = null as any;
e.server.gatewayServerUri = null;
e.server.discoveryProfileUri = null;
e.server.discoveryUrls = null;
e.serverCertificate = null as any;
return e;
}

Expand Down Expand Up @@ -1314,9 +1319,9 @@ export class OPCUAServer extends OPCUABaseServer {
): boolean {
const clientCertificate = channel.receiverCertificate!;
const securityPolicy = channel.messageBuilder.securityPolicy;
const serverCertificateChain = this.getCertificateChain();
const serverCertificate = this.getCertificate();

const result = verifySignature(serverCertificateChain, session.nonce!, clientSignature, clientCertificate, securityPolicy);
const result = verifySignature(serverCertificate, session.nonce!, clientSignature, clientCertificate, securityPolicy);

return result;
}
Expand Down

0 comments on commit 0fb4ee3

Please sign in to comment.