Skip to content

Instantly share code, notes, and snippets.

@svanellewee
Last active June 19, 2020 06:50
Show Gist options
  • Save svanellewee/0e0d83d68f939064c626eb454473873d to your computer and use it in GitHub Desktop.
Save svanellewee/0e0d83d68f939064c626eb454473873d to your computer and use it in GitHub Desktop.
PKI unittests copied from tutorial from the Blog post here https://ericchiang.github.io/post/go-tls/ Thanks Eric!

Making a CSR

You provide a Certificate Signing Request (CSR) by providing all the details that makes you a special little snowflake. You also provide your own public key as part of the package CSR.

In order to prove you’re one in a million though you also need to do something to the doc that only YOU can do.

That would be signing it digitially with YOUR private key:

  • Take cert request info bytes and hash it.
  • Take that hash and sign it with your private key.
  • This marks the document’s contents as coming from you and only you.

WHY DO I CARE?

When you sign some data with a private key (that only you have) the moment someone naughty modifies the document, the signature (that your private key produced) will no longer be valid for that document. It’s like changing a doc and seeing the hash change.

Only you can sign things with that private key. Only you light up the room when you walk into it.

The public key can be used to verify that it’s from your private key that signed the data, but it can’t be used re-sign a document after it was changed.

Usage example

Here’s the example node-forge provides for generating a CSR. Firstly you need to generate a keypair. This pair can be used to sign things and verify it already!

It always seems to be the step that starts your PKI journey.[fn:disclaimer1]

var forge = require('..');

console.log('Generating 1024-bit key-pair...');
var keys = forge.pki.rsa.generateKeyPair(1024);
console.log('Key-pair created.');

Now create a new CSR and populate it with the public key and some personal info. Not, “Do you like piña coladas? Walks in the rain?” More like who you are and where you’re located.

console.log('Creating certification request (CSR) ...');
var csr = forge.pki.createCertificationRequest();
csr.publicKey = keys.publicKey; // -------------------------> Look a public key!
csr.setSubject([{
  name: 'commonName',
  value: 'my-little-website.org'
}, {
  name: 'countryName',
  value: 'ZA'
}, {
  shortName: 'ST',
  value: 'Virginia'
}, {
  name: 'localityName',
  value: 'Blacksburg'
}, {
  name: 'organizationName',
  value: 'Brony Inc'
}, {
  shortName: 'OU',
  value: 'Test'
}]);
// add optional attributes
csr.setAttributes([{
  name: 'challengePassword',
  value: 'password'
}, {
  name: 'unstructuredName',
  value: 'My company'
}]);

Now use that private key (and some hash algo that you pick) and sign that sucker.

// sign certification request
csr.sign(keys.privateKey/*, forge.md.sha256.create()*/); // ---------> ♫ "I am the one and only..." ♫
console.log('Certification request (CSR) created.');

Now you can send the CSR to the CA and await a signed cert back.

Problem: How does the CA know it’s legit?

Let’s review what the CA now has of you:

From the CSR they have

  1. your public key.
  2. your signature (made with your private key)
    • NOTE you don’t send the private key also, just the signature.
  3. all your org’s personal deets.

The CA can verify that the signature comes only from you using the public key. This uses MATH.[fn:math]

Assuming the CA lives in node land and uses node-forge this is the sort of code they’d run:

// verify certification request
try {
  if(csr.verify()) {
    console.log('Certification request (CSR) verified.');
  } else {
    throw new Error('Signature not verified.');
  }
} catch(err) {
  console.log('Certification request (CSR) verification failure: ' +
    JSON.stringify(err, null, 2));
}

If csr.verify() results in a true the the CA will be convinced you sent them this CSR using your private key.

The machinery behind csr signing was implemented in node-forge csr as follows:

#+NAME signing a csr

csr.sign = function(key, md) {
  // TODO: get signature OID from private key
  csr.md = md || forge.md.sha1.create();
  var algorithmOid = oids[csr.md.algorithm + 'WithRSAEncryption'];
  if(!algorithmOid) {
    var error = new Error('Could not compute certification request digest. ' +
      'Unknown message digest algorithm OID.');
    error.algorithm = csr.md.algorithm;
    throw error;
  }
  csr.signatureOid = csr.siginfo.algorithmOid = algorithmOid;

  // get CertificationRequestInfo, convert to DER
  csr.certificationRequestInfo = pki.getCertificationRequestInfo(csr);
  var bytes = asn1.toDer(csr.certificationRequestInfo);

  // digest and sign
  csr.md.update(bytes.getBytes());
  csr.signature = key.sign(csr.md);
};

[fn:math] Since I’m not a nerd I’m not bothered with that right now, I just want the MS Excel poweruser level understanding of how things work in PKI.[fn:jk] [fn:jk] I keed, some of my best friends are MS Excel Powerusers. [fn:disclaimer1] At least in my limited experience

package main_test
import (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"errors"
"fmt"
"log"
"math/big"
"net"
"net/http"
"net/http/httptest"
"net/http/httputil"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
// Self imposed tutorial from the Blog post here https://ericchiang.github.io/post/go-tls/
// Thanks Eric!
func TestSomething(t *testing.T) {
t.Run("encode with public and decode using private key", func(t *testing.T) {
// Make private and public key
privKey, err := rsa.GenerateKey(rand.Reader, 2048)
assert.Nil(t, err)
plainText := []byte("the rain in spain stays mainly in the plane")
cipherText, err := rsa.EncryptPKCS1v15(rand.Reader, &privKey.PublicKey, plainText)
assert.Nil(t, err)
decodedMessage, err := rsa.DecryptPKCS1v15(rand.Reader, privKey, cipherText)
assert.Nil(t, err)
assert.Equal(t, decodedMessage, plainText)
// The cool part about this is you can prove you hold a private key without ever showing it to somebody.
})
t.Run("you can sign some content", func(t *testing.T) {
privKey, err := rsa.GenerateKey(rand.Reader, 2048)
assert.Nil(t, err)
plainText := []byte("Folk magic, hot off the grill")
hash := sha256.Sum256(plainText)
fmt.Printf("%#x\n", hash)
// make a signature using the private key and some HASHED content
signature, err := rsa.SignPKCS1v15(rand.Reader, privKey, crypto.SHA256, hash[:])
assert.Nil(t, err)
verifyUsingPublicKey := func(pub *rsa.PublicKey, msg, signature []byte) error {
aHash := sha256.Sum256(msg)
return rsa.VerifyPKCS1v15(pub, crypto.SHA256, aHash[:], signature)
}
// Did privKey sign this ?
// this won't work
assert.Error(t, verifyUsingPublicKey(&privKey.PublicKey, []byte("hello world"), signature))
// this won't work either
assert.Error(t, verifyUsingPublicKey(&privKey.PublicKey, []byte("Folk magic, something something"), signature))
// this is legit..
assert.Nil(t, verifyUsingPublicKey(&privKey.PublicKey, plainText, signature))
// This might be very helpful for say, a certificate authority, who wants to be able to distribute documents which can't be altered without everyone detecting.
})
t.Run("now to make a cert and break the client", func(t *testing.T) {
// generate a new key-pair
rootKey, err := rsa.GenerateKey(rand.Reader, 2048)
assert.Nil(t, err)
rootCert, rootCertPEM, err := makeRootCert(rootKey)
assert.Nil(t, err)
fmt.Printf("%s\n", rootCertPEM)
//fmt.Printf("%#x\n", rootCert.Signature)
servKey, err := rsa.GenerateKey(rand.Reader, 2048)
assert.Nil(t, err)
_, servCertPEM, err := makeServCert(rootKey, rootCert, servKey)
// PEM encode the private key
servKeyPEM := pem.EncodeToMemory(&pem.Block{
Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(servKey),
})
// Create a TLS cert using the private key and certificate
servTLSCert, err := tls.X509KeyPair(servCertPEM, servKeyPEM)
assert.Nil(t, err)
ok := func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("HI!")) }
s := httptest.NewUnstartedServer(http.HandlerFunc(ok))
// Configure the server to present the certficate we created
s.TLS = &tls.Config{
Certificates: []tls.Certificate{servTLSCert},
}
// Make a new client
//certPool := x509.NewCertPool()
//certPool.AppendCertsFromPEM(rootCertPEM)
// configure a client to use trust those certificates
client := &http.Client{
// Transport: &http.Transport{
// TLSClientConfig: &tls.Config{RootCAs: certPool},
// },
}
s.StartTLS()
_, clientErr := client.Get(s.URL)
assert.NotNil(t, clientErr)
s.Close()
})
t.Run("now to make a cert and get the client to work", func(t *testing.T) {
// generate a new key-pair
rootKey, err := rsa.GenerateKey(rand.Reader, 2048)
assert.Nil(t, err)
rootCert, rootCertPEM, err := makeRootCert(rootKey)
assert.Nil(t, err)
fmt.Printf("%s\n", rootCertPEM)
//fmt.Printf("%#x\n", rootCert.Signature)
servKey, err := rsa.GenerateKey(rand.Reader, 2048)
assert.Nil(t, err)
_, servCertPEM, err := makeServCert(rootKey, rootCert, servKey)
// PEM encode the private key
servKeyPEM := pem.EncodeToMemory(&pem.Block{
Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(servKey),
})
// Create a TLS cert using the private key and certificate
servTLSCert, err := tls.X509KeyPair(servCertPEM, servKeyPEM)
assert.Nil(t, err)
ok := func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("HI!")) }
s := httptest.NewUnstartedServer(http.HandlerFunc(ok))
// Configure the server to present the certficate we created
s.TLS = &tls.Config{
Certificates: []tls.Certificate{servTLSCert},
}
// Make a new client
certPool := x509.NewCertPool()
certPool.AppendCertsFromPEM(rootCertPEM)
// configure a client to use trust those certificates
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{RootCAs: certPool},
},
}
s.StartTLS()
resp, clientErr := client.Get(s.URL)
assert.Nil(t, clientErr)
s.Close()
// fmt.Println(resp, clientErr, "bla")
dump, err := httputil.DumpResponse(resp, true)
assert.Nil(t, err)
fmt.Printf("%s\n", dump)
})
t.Run("now to make a cert and get the client to work (with MTLS)", func(t *testing.T) {
// generate a new key-pair
rootKey, err := rsa.GenerateKey(rand.Reader, 2048)
assert.Nil(t, err)
rootCert, rootCertPEM, err := makeRootCert(rootKey)
assert.Nil(t, err)
fmt.Printf("%s\n", rootCertPEM)
servKey, err := rsa.GenerateKey(rand.Reader, 2048)
assert.Nil(t, err)
_, servCertPEM, err := makeServCert(rootKey, rootCert, servKey)
// PEM encode the private key
servKeyPEM := pem.EncodeToMemory(&pem.Block{
Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(servKey),
})
// Create a TLS cert using the private key and certificate
servTLSCert, err := tls.X509KeyPair(servCertPEM, servKeyPEM)
assert.Nil(t, err)
ok := func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("HI!")) }
s := httptest.NewUnstartedServer(http.HandlerFunc(ok))
certPool := x509.NewCertPool()
certPool.AppendCertsFromPEM(rootCertPEM)
// Configure the server to present the certficate we created
s.TLS = &tls.Config{
Certificates: []tls.Certificate{servTLSCert},
ClientAuth: tls.RequireAndVerifyClientCert,
ClientCAs: certPool,
}
// Make a new client
clientKey, err := rsa.GenerateKey(rand.Reader, 2048)
assert.Nil(t, err)
_, clientCertPEM, err := makeClientCert(rootKey, rootCert, clientKey)
assert.Nil(t, err)
// encode and load the cert and private key for the client
clientKeyPEM := pem.EncodeToMemory(&pem.Block{
Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(clientKey),
})
clientTLSCert, err := tls.X509KeyPair(clientCertPEM, clientKeyPEM)
assert.Nil(t, err)
// configure a client to use trust those certificates
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: certPool,
Certificates: []tls.Certificate{clientTLSCert},
},
},
}
s.StartTLS()
resp, clientErr := client.Get(s.URL)
assert.Nil(t, clientErr)
s.Close()
// fmt.Println(resp, clientErr, "bla")
dump, err := httputil.DumpResponse(resp, true)
assert.Nil(t, err)
fmt.Printf("%s\n", dump)
})
}
// This is a helper function to create a cert template with a serial number and other required fields.
// Certificates are just public keys with some extra information.
//
func certTemplate() (*x509.Certificate, error) {
// generate a random serial number (a real cert authority would have some logic behind this)
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
return nil, errors.New("failed to generate serial number: " + err.Error())
}
// Mantra of the day...
// - Certificates are public keys with some attached information (like what domains they work for).
// - In order to create a certificate, we need to both specify that information and provide a public key.
tmpl := x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{Organization: []string{"Yhat, Inc."}},
SignatureAlgorithm: x509.SHA256WithRSA,
NotBefore: time.Now(),
NotAfter: time.Now().Add(time.Hour), // valid for an hour
BasicConstraintsValid: true,
}
return &tmpl, nil
}
//Certificates must be signed by the private key of a parent certificate.
func createCert(template, parent *x509.Certificate, pub interface{}, parentPriv interface{}) (cert *x509.Certificate, certPEM []byte, err error) {
certDER, err := x509.CreateCertificate(rand.Reader, template, parent, pub, parentPriv)
if err != nil {
return
}
// parse the resulting certificate so we can use it again
cert, err = x509.ParseCertificate(certDER)
if err != nil {
return
}
// PEM encode the certificate (this is a standard TLS encoding)
b := pem.Block{Type: "CERTIFICATE", Bytes: certDER}
certPEM = pem.EncodeToMemory(&b)
return
}
// Certificates must be signed by the private key of a parent certificate,
// of course, there always has to be a certificate without a parent,
// and in these cases the certificate's private key must be used in lieu of a parent's.
//
func makeRootCert(rootKey *rsa.PrivateKey) (cert *x509.Certificate, certPEM []byte, err error) {
rootCertTmpl, err := certTemplate()
if err != nil {
return
}
rootCertTmpl.IsCA = true
rootCertTmpl.KeyUsage = x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature
rootCertTmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}
rootCertTmpl.IPAddresses = []net.IP{net.ParseIP("127.0.0.1")}
return createCert(rootCertTmpl, rootCertTmpl, &rootKey.PublicKey, rootKey)
}
func makeServCert(parentKey *rsa.PrivateKey, parentCert *x509.Certificate, servKey *rsa.PrivateKey) (cert *x509.Certificate, certPEM []byte, err error) {
// create a template for the server
servCertTmpl, err := certTemplate()
if err != nil {
log.Fatalf("creating cert template: %v", err)
}
servCertTmpl.KeyUsage = x509.KeyUsageDigitalSignature
servCertTmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}
servCertTmpl.IPAddresses = []net.IP{net.ParseIP("127.0.0.1")}
return createCert(servCertTmpl, parentCert, &servKey.PublicKey, parentKey)
}
func makeClientCert(parentKey *rsa.PrivateKey, parentCert *x509.Certificate, clientKey *rsa.PrivateKey) (cert *x509.Certificate, certPEM []byte, err error) {
// create a template for the client
clientCertTmpl, err := certTemplate()
if err != nil {
log.Fatalf("creating cert template: %v", err)
}
clientCertTmpl.KeyUsage = x509.KeyUsageDigitalSignature
clientCertTmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}
return createCert(clientCertTmpl, parentCert, &clientKey.PublicKey, parentKey)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment