commit 9cc696353d2bcbee89e6501f466afaa4aa7e6a37 Author: Ross Kinder Date: Tue Oct 6 09:55:45 2015 -0400 initial commit, xmldsig support diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..4da225f --- /dev/null +++ b/.travis.yml @@ -0,0 +1,10 @@ +language: go + +addons: + apt: + packages: + - libxml2-dev + - libxmlsec1-dev + +go: + - 1.5 diff --git a/README.md b/README.md new file mode 100644 index 0000000..285cf6d --- /dev/null +++ b/README.md @@ -0,0 +1,52 @@ +# go-xmlsec + +A (partial) wrapper for [xmlsec](https://www.aleksey.com/xmlsec). + +## Signing Example + + key := []byte(`-----BEGIN PRIVATE KEY----- + MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBAOK9uFHs/nXrH9Lc + GorG6lB7Qs42iWK6mIE56wI7dIdsOuXf6r0ht+d+YTTis24xw+wjEHXrVN0Okh6w + sKftzxo8chIo60+UB5NlKdvxAC7tpGNmrf49us/m5bdNx8IY+0pPK0c6B786Uluj + Tvx1WFdDXh3UQPBclbWtFe5S3gLxAgMBAAECgYAPj9ngtZVZXoPWowinUbOvRmZ1 + ZMTVI91nsSPyCUacLM92C4I+7NuEZeYiDRUnkP7TbCyrCzXN3jwlIxdczzORhlXB + Bgg9Sw2fkV61CnDEMgw+aEeD5A0GDA6eTwkrawiOMs8vupjsi2/stPsa+bmpI6Rn + fdEKBdyDP6iQQhAxiQJBAPNtM7IMvRzlZBXoDaTTpP9rN2FR0ZcX0LT5aRZJ81qi + +ZOBFeHUb6MyWvzZKfPinj9JO3s/9e3JbMXemRWBmvcCQQDuc+NfAeW200QyjoC3 + Ed3jueLMrY1Q3zTcSUhRPw/0pIKgRGZJerro8N6QY2JziV2mxK855gKTwwBigMHL + 2S9XAkEAwuBfjGDqXOG/uFHn6laNNvWshjqsIdus99Tbrj5RlfP2/YFP9VTOcsXz + VYy9K0P3EA8ekVLpHQ4uCFJmF3OEjQJBAMvwO69/HOufhv1CWZ25XzAsRGhPqsRX + Eouw9XPfXpMavEm8FkuT9xXRJFkTVxl/i6RdJYx8Rwn/Rm34t0bUKqMCQQCrAtKC + Un0PLcemAzPi8ADJlbMDG/IDXNbSej0Y4tw9Cdho1Q38XLZJi0RNdNvQJD1fWu3x + 9+QU/vJr7lMLzdoy + -----END PRIVATE KEY-----`) + + docStr := ` + + + + Hello, World! + + + + + + + + + + + + + + + + + + + ` + + signedDoc, err := xmldsig.Sign(key, doc) + os.Stdout.Write(signedDoc) diff --git a/xmldsig/xmldsig.go b/xmldsig/xmldsig.go new file mode 100644 index 0000000..bfd1af3 --- /dev/null +++ b/xmldsig/xmldsig.go @@ -0,0 +1,163 @@ +package xmldsig + +import ( + "errors" + "unsafe" +) + +// Note: on mac you need: brew install libxmlsec1 libxml2 + +// #cgo pkg-config: xmlsec1 +// #include +// #include +// #include +// #include +import "C" + +// #cgo pkg-config: libxml-2.0 +// #include "libxml/parser.h" +// #include "libxml/parserInternals.h" +// #include "libxml/xmlmemory.h" +import "C" + +func init() { + C.xmlInitParser() + + if rv := C.xmlSecInit(); rv < 0 { + panic("xmlsec failed to initialize") + } + if rv := C.xmlSecCryptoAppInit(nil); rv < 0 { + panic("xmlsec crypto initialization failed.") + } + if rv := C.xmlSecCryptoInit(); rv < 0 { + panic("xmlsec crypto initialization failed.") + } +} + +func newContext(pemFormatKey []byte) (*C.xmlSecDSigCtx, error) { + ctx := C.xmlSecDSigCtxCreate(nil) + if ctx == nil { + return nil, errors.New("failed to create signature context") + } + + ctx.signKey = C.xmlSecCryptoAppKeyLoadMemory( + (*C.xmlSecByte)(unsafe.Pointer(&pemFormatKey[0])), + C.xmlSecSize(len(pemFormatKey)), + C.xmlSecKeyDataFormatPem, + nil, nil, nil) + if ctx.signKey == nil { + return nil, errors.New("failed to load pem key") + } + return ctx, nil +} + +func closeContext(ctx *C.xmlSecDSigCtx) { + C.xmlSecDSigCtxDestroy(ctx) +} + +func newDoc(s string) (*C.xmlDoc, error) { + ctx := C.xmlCreateMemoryParserCtxt(C.CString(s), C.int(len(s))) + if ctx == nil { + return nil, errors.New("error creating parser") + } + defer C.xmlFreeParserCtxt(ctx) + + //C.xmlCtxtUseOptions(ctx, C.int(p.Options)) + C.xmlParseDocument(ctx) + + if ctx.wellFormed == C.int(0) { + return nil, errors.New("malformed XML") + } + + doc := ctx.myDoc + if doc == nil { + return nil, errors.New("parse failed") + } + return doc, nil +} + +func dumpDoc(doc *C.xmlDoc) string { + var buffer *C.xmlChar + var bufferSize C.int + C.xmlDocDumpMemory(doc, &buffer, &bufferSize) + rv := C.GoStringN((*C.char)(unsafe.Pointer(buffer)), bufferSize) + C.xmlMemFree(unsafe.Pointer(buffer)) + return rv +} + +func closeDoc(doc *C.xmlDoc) { + C.xmlFreeDoc(doc) +} + +// Sign returns a version of docStr signed with key according to +// the XML-DSIG standard. docStr is a template document meaning +// that it contains a `Signature` element in the +// http://www.w3.org/2000/09/xmldsig# namespace. +func Sign(key []byte, docStr string) (string, error) { + ctx, err := newContext(key) + if err != nil { + return "", err + } + defer closeContext(ctx) + + doc, err := newDoc(docStr) + if err != nil { + return "", err + } + defer closeDoc(doc) + + node := C.xmlSecFindNode(C.xmlDocGetRootElement(doc), + (*C.xmlChar)(unsafe.Pointer(&C.xmlSecNodeSignature)), + (*C.xmlChar)(unsafe.Pointer(&C.xmlSecDSigNs))) + if node == nil { + return "", errors.New("cannot find start node") + } + + if rv := C.xmlSecDSigCtxSign(ctx, node); rv < 0 { + return "", errors.New("failed to sign") + } + + return dumpDoc(doc), nil +} + +var ErrVerificationFailed = errors.New("signature verification failed") + +const ( + xmlSecDSigStatusUnknown = 0 + xmlSecDSigStatusSucceeded = 1 + xmlSecDSigStatusInvalid = 2 +) + +// Verify checks that the signature in docStr is valid according +// to the XML-DSIG specification. publicKey is the public part of +// the key used to sign docStr. If the signature is not correct, +// this function returns ErrVarificationFailed. +func Verify(publicKey []byte, docStr string) error { + ctx, err := newContext(publicKey) + if err != nil { + return err + } + defer closeContext(ctx) + + doc, err := newDoc(docStr) + if err != nil { + return err + } + defer closeDoc(doc) + + node := C.xmlSecFindNode(C.xmlDocGetRootElement(doc), + (*C.xmlChar)(unsafe.Pointer(&C.xmlSecNodeSignature)), + (*C.xmlChar)(unsafe.Pointer(&C.xmlSecDSigNs))) + if node == nil { + return errors.New("cannot find start node") + } + + if rv := C.xmlSecDSigCtxVerify(ctx, node); rv < 0 { + return errors.New("failed to verify") + } + + if ctx.status != xmlSecDSigStatusSucceeded { + return ErrVerificationFailed + } + return nil +} diff --git a/xmldsig/xmldsig_test.go b/xmldsig/xmldsig_test.go new file mode 100644 index 0000000..2e8a95c --- /dev/null +++ b/xmldsig/xmldsig_test.go @@ -0,0 +1,77 @@ +package xmldsig + +import ( + "strings" + "testing" +) + +func TestSign(t *testing.T) { + key := []byte(`-----BEGIN PRIVATE KEY----- +MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBAOK9uFHs/nXrH9Lc +GorG6lB7Qs42iWK6mIE56wI7dIdsOuXf6r0ht+d+YTTis24xw+wjEHXrVN0Okh6w +sKftzxo8chIo60+UB5NlKdvxAC7tpGNmrf49us/m5bdNx8IY+0pPK0c6B786Uluj +Tvx1WFdDXh3UQPBclbWtFe5S3gLxAgMBAAECgYAPj9ngtZVZXoPWowinUbOvRmZ1 +ZMTVI91nsSPyCUacLM92C4I+7NuEZeYiDRUnkP7TbCyrCzXN3jwlIxdczzORhlXB +Bgg9Sw2fkV61CnDEMgw+aEeD5A0GDA6eTwkrawiOMs8vupjsi2/stPsa+bmpI6Rn +fdEKBdyDP6iQQhAxiQJBAPNtM7IMvRzlZBXoDaTTpP9rN2FR0ZcX0LT5aRZJ81qi ++ZOBFeHUb6MyWvzZKfPinj9JO3s/9e3JbMXemRWBmvcCQQDuc+NfAeW200QyjoC3 +Ed3jueLMrY1Q3zTcSUhRPw/0pIKgRGZJerro8N6QY2JziV2mxK855gKTwwBigMHL +2S9XAkEAwuBfjGDqXOG/uFHn6laNNvWshjqsIdus99Tbrj5RlfP2/YFP9VTOcsXz +VYy9K0P3EA8ekVLpHQ4uCFJmF3OEjQJBAMvwO69/HOufhv1CWZ25XzAsRGhPqsRX +Eouw9XPfXpMavEm8FkuT9xXRJFkTVxl/i6RdJYx8Rwn/Rm34t0bUKqMCQQCrAtKC +Un0PLcemAzPi8ADJlbMDG/IDXNbSej0Y4tw9Cdho1Q38XLZJi0RNdNvQJD1fWu3x +9+QU/vJr7lMLzdoy +-----END PRIVATE KEY-----`) + + docStr := ` + + + + Hello, World! + + + + + + + + + + + + + + + + + + +` + + expectedSignedString := "\n\n\n \n\tHello, World!\n \n \n \n \n \n \n \n \n \n \n 9H/rQr2Axe9hYTV2n/tCp+3UIQQ=\n \n \n 2rM7C8ZzCjxEY4kueUaSevvEZjORQ7hBTWGxUJXStyQScLtX1drFx9dRmUdk/uRr\n0O37B3gsbKzlpQNfdVYPIfWgswjEVLBH7Ncl1dJ6dTofkQrogIF5CQE+PIAG3MPh\nnWsIcBahRQ+rNaRB/TDscuEV3+V3Je4K7E0OEKEuP1I=\n \n\t\n \n \n\n" + + actualSignedString, err := Sign(key, docStr) + if err != nil { + t.Errorf("sign: %s", err) + return + } + + if actualSignedString != expectedSignedString { + t.Errorf("signed: expected %q, got `%q`", expectedSignedString, actualSignedString) + return + } + + if err := Verify(key, expectedSignedString); err != nil { + t.Errorf("verify: %s", err) + return + } + + brokenDoc := strings.Replace(expectedSignedString, "Hello", "Goodbye", 1) + err = Verify(key, brokenDoc) + if err != ErrVerificationFailed { + t.Errorf("verify: expected verification failed, got %s", err) + return + } +}