Home   Package List   Routine Alphabetical List   Global Alphabetical List   FileMan Files List   FileMan Sub-Files List   Package Component Lists   Package-Namespace Mapping  
Routine: XUDSIGVALIDATE

XUDSIGVALIDATE.m

Go to the documentation of this file.
XUDSIGVALIDATE ;OAK/WJY - XML digital signature validation; May 6, 2025@13:37
 ;;8.0;KERNEL;**824**;Jul 10, 1995;Build 11
 ;Per VA Directive 6402, this routine should not be modified.
 ;;
 Q
 ; Checks the validity of a signed XML document
 ; Input: XMLFILEPATH   = (Required) The path of a signed XML document
 ;        PUBLICKEYPATH = (Optional) The path of the certificate that corresponds with the private key that signed the XML document
 ;                         If no public key is passed here, the code will default to using the certificate in the signed XML document
 ;        IDNAME        = (Optional) If a specific element of the XML was signed (rather than the entire payload), you must specific the name
 ;                         of the ID that is referenced by the XML signature.
 ;        IDELEMENT     = (Optional) If a specific element of the XML was signed, you must specify which element was signed here.
 ;        ELAPSEDTIMEMS = (Optional) The amount of time that this function took to complete, in milliseconds.
 ;
 ; Output: 1 If the XML digital signature is valid
 ;         0 If the XML digital signature is invalid or if an error occurred
 ;        -1 The operating system is not supported
VALIDATESIG(XMLFILEPATH,PUBLICKEYPATH,IDNAME,IDELEMENT,ELAPSEDTIMEMS) ;
 N SC,START,END,FLAGS,FLAGPREFIX,ARGS,PATH,FILE,GUID,CERT,CMD,FULLCMD
 S $ETRAP="D ERR^XUDSIGVALIDATE Q 0"
 ;
 ; check operating system
 I $$OS^%ZOSV'="UNIX" Q -1
 ; start a timer
 S START=$ZH
 ; 
 ; check inputs
 I '$D(IDNAME)!'$D(IDELEMENT) S IDNAME="" S IDELEMENT=""
 I '$$VALIDATEINPUT(IDNAME)!'$$VALIDATEINPUT(IDELEMENT) Q 0
 I '(##CLASS(%Library.File).Exists(XMLFILEPATH)) Q 0
 I $D(PUBLICKEYPATH) S EXISTS=##CLASS(%Library.File).Exists(PUBLICKEYPATH) I 'EXISTS Q 0
 ;
 ; build the linux command
 S CMD="xmlsec1 --verify"
 I '$D(PUBLICKEYPATH) D
 . S GUID=$System.Util.CreateGUID()
 . S CERT=$$GETCERTIFICATE(XMLFILEPATH)
 . S PATH="/tmp/vista-kernel-dsigcert-"_GUID_""
 . D SAVETOFILE(PATH,CERT)
 . S PUBLICKEYPATH=PATH
 I $D(PUBLICKEYPATH) S FLAGS("pubkey-cert-pem")=PUBLICKEYPATH S FLAGS("enabled-key-data")="raw-x509-cert"
 I ((IDNAME'="")&(IDELEMENT'="")) S FLAGS("id-attr:"_IDNAME)=IDELEMENT
 S ARGS("XMLFILEPATH")=XMLFILEPATH
 S FLAGPREFIX="--"
 S FULLCMD=$$BUILDCMD(CMD,.FLAGS,FLAGPREFIX,.ARGS)
 ;
 ; run the command
 S SC=$ZF(-1,FULLCMD)
 S END=$ZH
 S ELAPSEDTIMEMS=(END-START)*100
 ;
 ; return 1 if successful, otherwise return 0
 I SC=0 Q 1
 Q 0
VERIFYCHAIN(CACERT,XMLFILEPATH,TEMPCERTPATH,CHECKINDCA,CASUBJECT,CACONTENT,ELAPSEDTIMEMS) ;
 ; Validates that the cert (with corresponding private key) used to sign the xml was generated by a specific cert (root CA or intermediate CA)
 ;
 ; Input:  CACERT           = (Required) A Certificate Authority or bundle of Certificate Authorities in PEM format.
 ;         XMLFILEPATH      = (Required) The file path of a signed XML document.
 ;         TEMPCERTFILEPATH = (Optional, By Reference) In order to verify a cert chain, the cert from the XML must be saved as a file.
 ;                            This param allows the user to specify a file location where the cert will be saved to.
 ;                            If left blank, the cert will be saved to a temp directory.
 ;                            If this param is pased by reference the file path will be returned.
 ;         CHECKINDCA       = (Optional) This is a boolean (1 or 0) that determines if the code will iterate through the list of certificates
 ;                            in a certificate bundle passed in CACERT. If set to 1, the code will look for a certficate whose subject matches the
 ;                            issuer of the certificate of the cert in the XML (TEMPCERTFILEPATH). If there is a match, the code will
 ;                            proceed to verify the certificate using openssl. If the openssl verify command succeeds, VERIFYCHAIN returns 1. Otherwise 0 is returned.
 ;                            If this parameter is left blank or set to 0, then the entire bundle in CACERT will be used for chain validation and if there is one match in the entire bundle,
 ;                            then verification will succeed.
 ;         CASUBJECT        = (Optional, By Reference) If CHECKINDCA is set to 1, and a successful match is found, then CASUBJECT will be set to the matching CA Subeject.
 ;         CACONTENT        = (Optional, By Reference) If CHECKINDCA is set to 1, and a successful match is found, then CACONTENT will be set to the certficate content of the matching CA.
 ;         ELAPSEDTIMEMS    = (Optional) The amount of time that this function took to complete, in milliseconds.
 ;
 ; Output: 1 On successful chain validation
 ;         0 Chain validation failed or an error occured
 ;        -1 The operating system is not supported
 N START,END,GUID,STATUS,CERT
 S $ETRAP="D ERR^XUDSIGVALIDATE Q 0"
 ; check operating system
 I $$OS^%ZOSV'="UNIX" Q -1
 S START=$ZH
 S GUID=$System.Util.CreateGUID()
 I '$D(TEMPCERTPATH) S TEMPCERTPATH="/tmp/vista-kernel-dsigcert-"_GUID_""
 I '$$CHECKPATH(TEMPCERTPATH)!'$$CHECKPATH(CACERT) Q 0
 I '##CLASS(%Library.File).Exists(CACERT) Q 0
 S CERT=$$GETCERTIFICATE(XMLFILEPATH)
 D SAVETOFILE(TEMPCERTPATH,CERT)
 I '$D(CHECKINDCA) S CHECKINDCA=0
 I CHECKINDCA=1 S STATUS=$$VERIFYINDCA(TEMPCERTPATH,CACERT,.CASUBJECT,.CACONTENT)
 I '$D(STATUS) S STATUS=$$VERIFYCA(CACERT,TEMPCERTPATH)
 S END=$ZH
 S ELAPSEDTIMEMS=(END-START)*100
 I STATUS=1 Q 1
 Q 0
 ; Builds a linux command that will be executed by $ZF
BUILDCMD(CMD,FLAGS,FLAGPREFIX,ARGS) ;
 N I,FULLCMD,KEY,ARG
 S FULLCMD=CMD
 S KEY=""
 F  S KEY=$O(FLAGS(KEY)) Q:KEY=""  D
 . I KEY[":"  D  S FULLCMD=FULLCMD_" "_FLAGPREFIX_KEY_" "_""""_FLAGS(KEY)_""""
 . E  I FLAGS(KEY)'=""  D  S FULLCMD=FULLCMD_" "_FLAGPREFIX_KEY_" "_""""_FLAGS(KEY)_""""
 . E  D  S FULLCMD=FULLCMD_" "_FLAGPREFIX_KEY
 . Q
 S ARG=""
 F  S ARG=$O(ARGS(ARG)) Q:ARG=""  D 
 . S FULLCMD=FULLCMD_" "_ARGS(ARG)
 . Q
 S FULLCMD=FULLCMD_" > /dev/null 2>&1"
 Q FULLCMD
 ; Given a leaf certificate path and a CA (or CA bundle), verify that the CA that generated the leaf certificate
 ; is present and perform the chain verification.
 ; 
 ; Input:  LEAFCERTPATH = (Required) The path of the leaf certificate (typically this is the certificate in the signed XML document)
 ;         CABUNDLE     = (Required) The bundle of CA certificates (can contain just 1 certificate)
 ;         SUBJECT      = (Optional, By Reference) The subject of the CA certificate that generated the leaf certificate
 ;         CACONTENT    = (Optional, By Reference) The contents of the CA certificate that generated the leaf certificate
 ; Output: 1 Success - If there was CA present in the CA bundle with a Subject equal to the Issuer of the leaf certificate,
 ;           and openssl verify determines that the CA generated the leaf certificate. 
 ;         0 Failure - All other scenarios
VERIFYINDCA(LEAFCERTPATH,CABUNDLE,SUBJECT,CACONTENT) ;
 N LEAFCERTFILE,LEAFCERT,LEAFCERTSUBJECT,CABUNDLEFILE,STARTTAG,ENDTAG,START,END,CERT,CAPATH,FILE,STATUS,DONE
 S LEAFCERTFILE=##CLASS(%Stream.FileCharacter).%New()
 D LEAFCERTFILE.LinkToFile(LEAFCERTPATH)
 S LEAFCERT=LEAFCERTFILE.Read($SYSTEM.SYS.MaxLocalLength())
 S LEAFCERTSUBJECT=$SYSTEM.Encryption.X509GetField(LEAFCERT,"Issuer")
 S CABUNDLEFILE=##CLASS(%Stream.FileCharacter).%New()
 D CABUNDLEFILE.LinkToFile(CABUNDLE)
 S CABUNDLE=CABUNDLEFILE.Read($SYSTEM.SYS.MaxLocalLength())
 S STARTTAG="-----BEGIN CERTIFICATE-----"
 S ENDTAG="-----END CERTIFICATE-----"
 S DONE=0
 S STATUS=0
 F  Q:DONE=1  D
 . S START=$F(CABUNDLE,STARTTAG)
 . I START=0 S DONE=1 Q
 . S START=START-$L(STARTTAG)
 . S END=$F(CABUNDLE,ENDTAG,START)
 . S END=END+$L(ENDTAG)-1
 . I END=0 S DONE=1 Q
 . S CERT=$E(CABUNDLE,START,END)
 . S SUBJECT=$SYSTEM.Encryption.X509GetField(CERT,"Subject")
 . I SUBJECT=LEAFCERTSUBJECT D
 . . S CAPATH=LEAFCERTPATH_"-CA"
 . . D SAVETOFILE^XUDSIGVALIDATE(CAPATH,CERT)
 . . S STATUS=$$VERIFYCA(CAPATH,LEAFCERTPATH)
 . . I STATUS=1 S CACONTENT=CERT S DONE=1 Q
 . S CABUNDLE=$E(CABUNDLE,END-$L(STARTTAG),*)
 I STATUS=1 Q 1
 S CACONTENT=""
 S SUBJECT=""
 Q 0
VERIFYCA(CACERT,TEMPCERTPATH) ;
 N VCMD,FLAGS,ARGS,FLAGPREFIX,STATUS
 S VCMD="openssl verify"
 S FLAGS("partial_chain")=""
 S FLAGS("CAfile")=CACERT
 S ARGS("0")=TEMPCERTPATH
 S FLAGPREFIX="-"
 S VCMD=$$BUILDCMD(VCMD,.FLAGS,FLAGPREFIX,.ARGS)
 S STATUS=$ZF(-1,VCMD)
 I STATUS=0 Q 1
 Q STATUS
 ; Retrieve the certificate embedded in a signed XML document.
 ; Typically the certificate is contained in the X509Certificate element.
 ; 
 ; Input:   XMLFILE = The path of the signed XML document.
 ; Output:  The X509 certificate that was contained in the signed XML document.
 ;          If no certificate was found, "" is returned.
GETCERTIFICATE(XMLFILE) ;
 N XPATHDOC,RESULTS,CERTIFICATE
 D ##CLASS(%XML.XPATH.Document).CreateFromFile(XMLFILE,.XPATHDOC)
 D XPATHDOC.EvaluateExpression("/","//*[local-name()='X509Certificate']/text()",.RESULTS)
 I RESULTS.Size'=1 Q ""
 S CERTIFICATE=RESULTS.Oref(1).Value
 S CERTIFICATE="-----BEGIN CERTIFICATE-----"_$C(13,10)_CERTIFICATE_$C(13,10)_"-----END CERTIFICATE-----"
 Q CERTIFICATE
SAVETOFILE(PATH,CONTENT) ;
 N FILE
 S FILE=##CLASS(%Stream.FileCharacter).%New()
 D FILE.LinkToFile(PATH)
 D FILE.Write(CONTENT)
 D FILE.%Save()
 Q
CHECKPATH(PATH) ;
 I PATH'?1"/".E Q 0
 I PATH?.E1(1"|",1"&",1";",1"`",1"$",1">",1"<",1"\",1"'").E Q 0
 ;I PATH'?1"/tmp/".E Q 0
 Q 1
VALIDATEINPUT(INPUT) ;
 I '$D(INPUT) Q 1
 I INPUT="" Q 1
 I $MATCH(INPUT,"^[A-Za-z_][A-za-z0-9._-]*$") Q 1
 Q 0
ERR ;
 D ^%ZTER
 Q