document generation, and signing are usable now
This commit is contained in:
parent
77ea548a50
commit
cee1fa8ece
|
|
@ -34,3 +34,10 @@ IRALEX_AUTH_PASSWORD=account_password
|
|||
# overwrites it if specified. If a port is not explicitly specified, then a
|
||||
# default port is used.
|
||||
PORT=8000
|
||||
|
||||
# Optional
|
||||
# Whether to enable the debugging features or not, such as extra logging,
|
||||
# different code path execution, etc. Any one of the following values is legal:
|
||||
# 1 = t = T = true = True = TRUE = yes
|
||||
# 0 = f = F = false = False = FALSE = no
|
||||
DEBUG=true
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@
|
|||
package config
|
||||
|
||||
const (
|
||||
DEBUG = true
|
||||
|
||||
PRODUCTION_URL = "https://api.myinvois.hasil.gov.my"
|
||||
SANDBOX_URL = "https://preprod-api.myinvois.hasil.gov.my"
|
||||
BASE_URL = PRODUCTION_URL
|
||||
|
|
|
|||
22
core/env.go
22
core/env.go
|
|
@ -2,8 +2,8 @@ package core
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"iralex-einvoice/config"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
|
|
@ -16,9 +16,10 @@ var (
|
|||
CLIENT_ID string
|
||||
CLIENT_SECRET_1 string
|
||||
CLIENT_SECRET_2 string
|
||||
CERTIFICATE_FILENAME string // path to a PKCS12 file e.g. cert.p12, static/1f92.p12, etc
|
||||
CERTIFICATE_PASSWORD string // the PIN of the aforementioned PKCS12 file
|
||||
PORT string // the port the program listens to
|
||||
CERTIFICATE_FILENAME string // path to a PKCS12 file e.g. cert.p12, static/1f92.p12, etc
|
||||
CERTIFICATE_PASSWORD string // the PIN of the aforementioned PKCS12 file
|
||||
PORT string // the port the program listens to
|
||||
DEBUG bool = false // whether to use the debug or production environment
|
||||
IRALEX_BACKEND_BASE_URL string
|
||||
IRALEX_AUTH_EMAIL string // email address used to get an access token from the Iralex backend
|
||||
IRALEX_AUTH_PASSWORD string // password used to get an access token from the Iralex backend
|
||||
|
|
@ -50,6 +51,7 @@ func SetUpEnvironmentVariables() {
|
|||
CERTIFICATE_PASSWORD = os.Getenv("CERTIFICATE_PASSWORD") // required
|
||||
|
||||
PORT = os.Getenv("PORT")
|
||||
_DEBUG := os.Getenv("DEBUG")
|
||||
|
||||
IRALEX_BACKEND_BASE_URL = os.Getenv("IRALEX_BACKEND_BASE_URL") // required
|
||||
IRALEX_AUTH_EMAIL = os.Getenv("IRALEX_AUTH_EMAIL") // optional
|
||||
|
|
@ -89,7 +91,15 @@ func SetUpEnvironmentVariables() {
|
|||
}
|
||||
|
||||
// Debug-only Required Environment Variables
|
||||
if config.DEBUG {
|
||||
if _DEBUG != "" {
|
||||
DEBUG, err = strconv.ParseBool(_DEBUG)
|
||||
if err != nil {
|
||||
fmt.Printf("\033[1;31mError: The environment variable DEBUG is specified but its value is incorrect.\033[0m\n")
|
||||
mustTerminate = true
|
||||
}
|
||||
}
|
||||
|
||||
if DEBUG {
|
||||
if IRALEX_BACKEND_BASE_URL == "" {
|
||||
fmt.Printf("\033[1;31mError: Missing required environment variable: IRALEX_BACKEND_BASE_URL\033[0m\n")
|
||||
mustTerminate = true
|
||||
|
|
@ -107,7 +117,7 @@ func SetUpEnvironmentVariables() {
|
|||
}
|
||||
|
||||
// Production-only Required Environment Variables
|
||||
if !config.DEBUG {
|
||||
if !DEBUG {
|
||||
// add more here...
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -247,12 +247,6 @@ func Generate(bill common.IralexBill) UBLInvoiceRoot {
|
|||
}
|
||||
|
||||
totalExcludingTax := amount.Sub(discountAmount)
|
||||
fmt.Println("Amount", amount)
|
||||
fmt.Println("Discount", discountAmount)
|
||||
fmt.Println("Tax Rate", decimal.NewFromInt(int64(bill.FirmSetting.Setting.Tax.Rate)))
|
||||
fmt.Println("amount - discount", amount.Sub(discountAmount))
|
||||
fmt.Println("amount - discount * tax rate", amount.Sub(discountAmount).Mul(decimal.NewFromInt(int64(bill.FirmSetting.Setting.Tax.Rate))))
|
||||
fmt.Println("amount - discount * tax rate / 100", amount.Sub(discountAmount).Mul(decimal.NewFromInt(int64(bill.FirmSetting.Setting.Tax.Rate))).Div(decimal.NewFromInt(100)))
|
||||
taxedAmount := amount.Sub(discountAmount).Mul(decimal.NewFromInt(int64(bill.FirmSetting.Setting.Tax.Rate))).Div(decimal.NewFromInt(100))
|
||||
|
||||
doc.Invoice[0].InvoiceLine[index].ID = []UBLText{{Value: strconv.Itoa(item.ItemNo)}}
|
||||
|
|
|
|||
195
document/sign.go
195
document/sign.go
|
|
@ -15,6 +15,7 @@ import (
|
|||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
orderedmap "github.com/wk8/go-ordered-map/v2"
|
||||
"golang.org/x/crypto/pkcs12"
|
||||
|
|
@ -22,15 +23,201 @@ import (
|
|||
|
||||
// Signs a document, populting its Signature and UBLExtensions sections. Returns
|
||||
// the signed document.
|
||||
func Sign(doc UBLInvoiceRoot) UBLInvoiceRoot {
|
||||
//
|
||||
// Pre-conditon:
|
||||
// 1) The specified document must be transformed, i.e. the `UBLExtensions` and
|
||||
// `Signature` keys must be removed, if they already exist, which will be
|
||||
// populated by this function.
|
||||
// 2) The specified document must have an Invoice[0] already, or you get paniced.
|
||||
func Sign(doc UBLInvoiceRoot) (UBLInvoiceRoot, error) {
|
||||
// -------------------------------------------------------------------------
|
||||
// Minify the transformed document and calculate the document digest.
|
||||
|
||||
docJson, _ := json.MarshalIndent(doc, "", " ")
|
||||
var docJsonMinified = &bytes.Buffer{}
|
||||
if err := json.Compact(docJsonMinified, docJson); err != nil {
|
||||
// Unexpected Error - This should never happen
|
||||
panic(fmt.Sprintf("Couldn't minify the document - %s\n", err.Error()))
|
||||
}
|
||||
fmt.Printf("Generated Document:\n%s\n", docJsonMinified)
|
||||
if core.DEBUG {
|
||||
core.LogInfo.Printf("INFO: Minified Transformed Document:\n%s\n", docJsonMinified)
|
||||
}
|
||||
|
||||
return doc
|
||||
hashedMinifiedDoc := sha256.Sum256(docJsonMinified.Bytes())
|
||||
var docDigest string = b64.StdEncoding.EncodeToString(hashedMinifiedDoc[:])
|
||||
if core.DEBUG {
|
||||
core.LogInfo.Printf("INFO: Document Digest:\n%s\n", docDigest)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Open the certificate file and sign the document digest with it.
|
||||
|
||||
privateKey, certificate, err := loadPKCS12(core.CERTIFICATE_FILENAME, core.CERTIFICATE_PASSWORD)
|
||||
if err != nil {
|
||||
return UBLInvoiceRoot{}, fmt.Errorf("cannot load the certificate file: %w", err)
|
||||
}
|
||||
|
||||
sign, err := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, hashedMinifiedDoc[:])
|
||||
if err != nil {
|
||||
return UBLInvoiceRoot{}, fmt.Errorf("cannot sign the doc digest with KCS1v15: %w", err)
|
||||
}
|
||||
|
||||
var cert_b64 string = b64.StdEncoding.EncodeToString(certificate.Raw)
|
||||
if core.DEBUG {
|
||||
core.LogInfo.Printf("INFO: Certificate Raw:\n%s\n", cert_b64)
|
||||
}
|
||||
|
||||
var cert_tbs_b64 string = b64.StdEncoding.EncodeToString(certificate.RawTBSCertificate)
|
||||
if core.DEBUG {
|
||||
core.LogInfo.Printf("INFO: Certificate RAW TBS:\n%s\n", cert_tbs_b64)
|
||||
}
|
||||
|
||||
var signature string = b64.StdEncoding.EncodeToString(sign)
|
||||
if core.DEBUG {
|
||||
core.LogInfo.Printf("INFO: Siganture:\n%s\n", signature)
|
||||
}
|
||||
|
||||
certificate_hash := sha256.Sum256(certificate.RawTBSCertificate)
|
||||
certDigest := b64.StdEncoding.EncodeToString(certificate_hash[:])
|
||||
if core.DEBUG {
|
||||
core.LogInfo.Printf("INFO: Certificate Digest:\n%s\n", certDigest)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Populating the signed properties section, minifying it, and calculating
|
||||
// its digest,
|
||||
|
||||
var subjectName = ""
|
||||
for _, atv := range certificate.Subject.Names {
|
||||
// parse the subject name
|
||||
oid := atv.Type.String()
|
||||
value := atv.Value.(string)
|
||||
switch oid {
|
||||
case "2.5.4.6": // country
|
||||
subjectName += fmt.Sprint("C=", value, ",")
|
||||
case "2.5.4.10": // organization name
|
||||
subjectName += fmt.Sprint("O=", value, ",")
|
||||
case "2.5.4.3": // common name
|
||||
subjectName += fmt.Sprint("CN=", value, ",")
|
||||
case "2.5.4.5": // serial number
|
||||
subjectName += fmt.Sprint("SERIALNUMBER=", value, ",")
|
||||
case "2.5.4.97": // organization identifier (tin)
|
||||
subjectName += fmt.Sprint("OID.2.5.4.97=", value, ",")
|
||||
case "1.2.840.113549.1.9.1": // email address
|
||||
subjectName += fmt.Sprint("E=", value, ",")
|
||||
}
|
||||
}
|
||||
|
||||
var qualifyingProperties = []UBLQualifyingProperties{{
|
||||
Target: "signature",
|
||||
SignedProperties: []UBLSignedProperties{{
|
||||
Id: "id-xades-signed-props",
|
||||
SignedSignatureProperties: []UBLSignedSignatureProperties{{
|
||||
SigningTime: []UBLText{{Value: time.Now().UTC().Format(time.RFC3339)}},
|
||||
SigningCertificate: []UBLSigningCertificate{{
|
||||
Cert: []UBLCert{{
|
||||
CertDigest: []UBLCertDigest{{
|
||||
DigestMethod: []UBLDigestMethod{{
|
||||
Value: "",
|
||||
Algorithm: "http://www.w3.org/2001/04/xmlenc#sha256",
|
||||
}},
|
||||
DigestValue: []UBLText{{
|
||||
Value: certDigest,
|
||||
}},
|
||||
}},
|
||||
IssuerSerial: []UBLIssuerSerial{{
|
||||
X509IssuerName: []UBLText{{Value: certificate.Issuer.String()}},
|
||||
X509SerialNumber: []UBLText{{Value: certificate.SerialNumber.String()}},
|
||||
}},
|
||||
}},
|
||||
}},
|
||||
}},
|
||||
}},
|
||||
}}
|
||||
|
||||
minifiedSignedProps, err := json.Marshal(qualifyingProperties)
|
||||
if err != nil {
|
||||
// Unexpected Error - This should never happen
|
||||
panic(fmt.Sprintf("Couldn't minify signed props - %s\n", err.Error()))
|
||||
}
|
||||
if core.DEBUG {
|
||||
core.LogInfo.Printf("INFO: Minified Signed Properties:\n%s\n", string(minifiedSignedProps))
|
||||
}
|
||||
|
||||
hashedMinifiedSignedProps := sha256.Sum256(minifiedSignedProps)
|
||||
var signedPropsDigest string = b64.StdEncoding.EncodeToString(hashedMinifiedSignedProps[:])
|
||||
if core.DEBUG {
|
||||
core.LogInfo.Printf("INFO: Signed Props Digest:\n%s\n", signedPropsDigest)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Populate the UBLExtensions and Signature sections of the document.
|
||||
|
||||
doc.Invoice[0].UBLExtensions = []UBLExtensionsWrapper{{
|
||||
UBLExtension: []UBLExtension{{
|
||||
ExtensionURI: []UBLText{{Value: "urn:oasis:names:specification:ubl:dsig:enveloped:xades"}},
|
||||
ExtensionContent: []UBLExtensionContent{{
|
||||
UBLDocumentSignatures: []UBLDocumentSignatures{{
|
||||
SignatureInformation: []UBLSignatureInformation{{
|
||||
ID: []UBLText{{Value: "urn:oasis:names:specification:ubl:signature:1"}},
|
||||
ReferencedSignatureID: []UBLText{{Value: "urn:oasis:names:specification:ubl:signature:Invoice"}},
|
||||
Signature: []UBLSignatureObj{{
|
||||
Id: "signature",
|
||||
Object: []UBLSignatureObject{{
|
||||
QualifyingProperties: qualifyingProperties,
|
||||
}},
|
||||
KeyInfo: []UBLKeyInfo{{
|
||||
X509Data: []UBLX509Data{{
|
||||
X509Certificate: []UBLText{{Value: cert_b64}},
|
||||
X509SubjectName: []UBLText{{Value: subjectName[:len(subjectName)-1]}},
|
||||
X509IssuerSerial: []UBLIssuerSerial{{
|
||||
X509IssuerName: []UBLText{{Value: certificate.Issuer.String()}},
|
||||
X509SerialNumber: []UBLText{{Value: certificate.SerialNumber.String()}},
|
||||
}},
|
||||
}},
|
||||
}},
|
||||
SignatureValue: []UBLText{{Value: signature}},
|
||||
SignedInfo: []UBLSignedInfo{{
|
||||
SignatureMethod: []UBLSignatureMethod{{
|
||||
Value: "",
|
||||
Algorithm: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
|
||||
}},
|
||||
Reference: []UBLReference{
|
||||
{
|
||||
Type: "http://uri.etsi.org/01903/v1.3.2#SignedProperties",
|
||||
URI: "#id-xades-signed-props",
|
||||
DigestMethod: []UBLDigestMethod{{
|
||||
Value: "",
|
||||
Algorithm: "http://www.w3.org/2001/04/xmlenc#sha256",
|
||||
}},
|
||||
DigestValue: []UBLText{{Value: signedPropsDigest}},
|
||||
},
|
||||
{
|
||||
Type: "",
|
||||
URI: "",
|
||||
DigestMethod: []UBLDigestMethod{{
|
||||
Value: "",
|
||||
Algorithm: "http://www.w3.org/2001/04/xmlenc#sha256",
|
||||
}},
|
||||
DigestValue: []UBLText{{Value: docDigest}},
|
||||
},
|
||||
},
|
||||
}},
|
||||
}},
|
||||
}},
|
||||
}},
|
||||
}},
|
||||
}},
|
||||
}}
|
||||
doc.Invoice[0].Signature = []UBLSignature{{
|
||||
ID: []UBLText{{Value: "urn:oasis:names:specification:ubl:signature:Invoice"}},
|
||||
SignatureMethod: []UBLText{{Value: "urn:oasis:names:specification:ubl:dsig:enveloped:xades"}},
|
||||
}}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Return the signed document.
|
||||
|
||||
return doc, nil
|
||||
}
|
||||
|
||||
// Signs the specified document.
|
||||
|
|
@ -51,8 +238,6 @@ func GenerateDocument(doc UBLInvoiceRoot) []byte {
|
|||
|
||||
// -------------------------------------------------------------------------
|
||||
// Transform the document.
|
||||
// The `UBLExtensions` and `Signature` keys must be removed, if they already
|
||||
// exist.
|
||||
|
||||
// var transformed_doc map[string]interface{}
|
||||
// _ = json.Unmarshal(doc_string, &transformed_doc)
|
||||
|
|
|
|||
|
|
@ -31,17 +31,17 @@ func SubmitDocument(accessToken string, doc document.UBLInvoiceRoot) {
|
|||
fmt.Println("Sending Request (/submit-document):\n-----------------------------------")
|
||||
|
||||
// Minify and convert the document to base64.
|
||||
docJson, _ := json.MarshalIndent(doc, "", " ")
|
||||
var minified_doc = &bytes.Buffer{}
|
||||
if err := json.Compact(minified_doc, docJson); err != nil {
|
||||
panic(err)
|
||||
minified_doc, err := json.Marshal(doc)
|
||||
if err != nil {
|
||||
// Unexpected Error - This should never happen
|
||||
panic(fmt.Sprintf("Couldn't minify signed props - %s\n", err.Error()))
|
||||
}
|
||||
|
||||
var sEnc string = b64.StdEncoding.EncodeToString(minified_doc.Bytes())
|
||||
var sEnc string = b64.StdEncoding.EncodeToString(minified_doc)
|
||||
|
||||
// Create a SHA256 hash of the document.
|
||||
hash := sha256.New()
|
||||
hash.Write(minified_doc.Bytes())
|
||||
hash.Write(minified_doc)
|
||||
|
||||
body, err := json.Marshal(requestBody{
|
||||
Documents: []requestDocument{
|
||||
|
|
|
|||
|
|
@ -151,10 +151,12 @@ const (
|
|||
// -------------------------------------------------------------------------
|
||||
// Monetary Calculation Errors
|
||||
|
||||
// add here...
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Miscellaneous Errors
|
||||
|
||||
_ERR_MISSING_FIRM_NAME = 550
|
||||
_ERR_MISSING_FIRM_NAME = 33
|
||||
_ERR_MISSING_FIRM_NAME_NAME = "Missing Firm Name"
|
||||
_ERR_MISSING_FIRM_NAME_MSG = "Your firm's name is not specified. Please add it from your settings."
|
||||
)
|
||||
|
|
|
|||
|
|
@ -106,7 +106,16 @@ func HandleSubmitDebug(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
doc := document.Generate(bill)
|
||||
doc = document.Sign(doc)
|
||||
doc, err = document.Sign(doc)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
fmt.Fprint(w, http.StatusText(http.StatusInternalServerError))
|
||||
core.LogInfo.Printf("ERROR: Cannot sign the document - %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
docJson, _ := json.MarshalIndent(doc, "", " ")
|
||||
fmt.Println(string(docJson))
|
||||
|
||||
return
|
||||
|
||||
|
|
@ -171,7 +180,13 @@ func HandleSubmit(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
doc := document.Generate(bill)
|
||||
doc = document.Sign(doc)
|
||||
doc, err = document.Sign(doc)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
fmt.Fprint(w, http.StatusText(http.StatusInternalServerError))
|
||||
core.LogInfo.Printf("ERROR: Cannot sign the document - %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
accessToken := route.AuthenticateFormData(core.CLIENT_ID, core.CLIENT_SECRET_1, core.TIN)
|
||||
route.SubmitDocument(accessToken, doc)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package server
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"iralex-einvoice/config"
|
||||
"iralex-einvoice/core"
|
||||
"log"
|
||||
"net/http"
|
||||
|
|
@ -31,7 +30,7 @@ func Run() {
|
|||
|
||||
http.HandleFunc("/ping", HandlePing)
|
||||
|
||||
if config.DEBUG {
|
||||
if core.DEBUG {
|
||||
http.HandleFunc("/submit", HandleSubmitDebug)
|
||||
} else {
|
||||
http.HandleFunc("/submit", HandleSubmit)
|
||||
|
|
|
|||
Loading…
Reference in New Issue