You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
ncdns/certdehydrate/certdehydrate.go

273 lines
8.6 KiB
Go

package certdehydrate
import (
"bytes"
"crypto/rand"
"crypto/sha256"
"crypto/x509"
"crypto/x509/pkix"
"encoding/base64"
"encoding/binary"
"encoding/json"
"fmt"
"math/big"
"time"
"github.com/namecoin/splicesign"
)
// A DehydratedCertificate represents the (nearly) minimal set of data required
// to deterministically construct a valid x509 certificate when combined with a
// domain name. See the Dehydrated TLS Certificates specification at:
// https://github.com/namecoin/proposals/blob/master/ifa-0003.md
// TODO: add a version field
type DehydratedCertificate struct {
PubkeyB64 string
NotBeforeScaled int64
NotAfterScaled int64
SignatureAlgorithm int64
SignatureB64 string
}
// SerialNumber calculates the certificate serial number according to the
// Dehydrated TLS Certificates specification.
func (dehydrated *DehydratedCertificate) SerialNumber(name string) ([]byte, error) {
nameHash := sha256.Sum256([]byte(name))
pubkeyBytes, err := base64.StdEncoding.DecodeString(dehydrated.PubkeyB64)
if err != nil {
return nil, fmt.Errorf("Dehydrated cert pubkey is not valid base64: %s", err)
}
pubkeyHash := sha256.Sum256(pubkeyBytes)
notBeforeScaledBuf := new(bytes.Buffer)
err = binary.Write(notBeforeScaledBuf, binary.BigEndian, dehydrated.NotBeforeScaled)
if err != nil {
return nil, fmt.Errorf("binary.Write of notBefore failed: %s", err)
}
notBeforeHash := sha256.Sum256(notBeforeScaledBuf.Bytes())
notAfterScaledBuf := new(bytes.Buffer)
err = binary.Write(notAfterScaledBuf, binary.BigEndian, dehydrated.NotAfterScaled)
if err != nil {
return nil, fmt.Errorf("binary.Write of notAfter failed: %s", err)
}
notAfterHash := sha256.Sum256(notAfterScaledBuf.Bytes())
serialHash := sha256.New()
_, err = serialHash.Write(nameHash[:])
if err != nil {
return nil, fmt.Errorf("serialHash.Write of nameHash failed: %s", err)
}
_, err = serialHash.Write(pubkeyHash[:])
if err != nil {
return nil, fmt.Errorf("serialHash.Write of pubkeyHash failed: %s", err)
}
_, err = serialHash.Write(notBeforeHash[:])
if err != nil {
return nil, fmt.Errorf("serialHash.Write of notBeforeHash failed: %s", err)
}
_, err = serialHash.Write(notAfterHash[:])
if err != nil {
return nil, fmt.Errorf("serialHash.Write of notAfterHash failed: %s", err)
}
// 19 bytes will be less than 2^159, see https://crypto.stackexchange.com/a/260
return serialHash.Sum(nil)[0:19], nil
}
func (dehydrated *DehydratedCertificate) String() string {
output := []interface{}{1, dehydrated.PubkeyB64, dehydrated.NotBeforeScaled, dehydrated.NotAfterScaled, dehydrated.SignatureAlgorithm, dehydrated.SignatureB64}
// We don't need to check for errors, because json.Marshal is guaranteed to
// succeed for integer/string types.
binOutput, _ := json.Marshal(output) //nolint:errchkjson
return string(binOutput)
}
// ParseDehydratedCert parses a dehydrated certificate from an interface as
// would be returned by a JSON Unmarshaler.
func ParseDehydratedCert(data interface{}) (*DehydratedCertificate, error) {
dehydrated, ok := data.([]interface{})
if !ok {
return nil, fmt.Errorf("Dehydrated cert is not a list")
}
if len(dehydrated) < 1 {
return nil, fmt.Errorf("Dehydrated cert must have a version field")
}
version, ok := dehydrated[0].(float64)
if !ok {
return nil, fmt.Errorf("Dehydrated cert version must be an integer")
}
if version != 1 {
return nil, fmt.Errorf("Dehydrated cert has an unrecognized version")
}
if len(dehydrated) < 6 {
return nil, fmt.Errorf("Dehydrated cert must have 6 items")
}
pubkeyB64, ok := dehydrated[1].(string)
if !ok {
return nil, fmt.Errorf("Dehydrated cert pubkey must be a string")
}
notBeforeScaled, ok := dehydrated[2].(float64)
if !ok {
return nil, fmt.Errorf("Dehydrated cert notBefore must be an integer")
}
notAfterScaled, ok := dehydrated[3].(float64)
if !ok {
return nil, fmt.Errorf("Dehydrated cert notAfter must be an integer")
}
signatureAlgorithm, ok := dehydrated[4].(float64)
if !ok {
return nil, fmt.Errorf("Dehydrated cert signature algorithm must be an integer")
}
signatureB64, ok := dehydrated[5].(string)
if !ok {
return nil, fmt.Errorf("Dehydrated cert signature must be a string")
}
result := DehydratedCertificate{
PubkeyB64: pubkeyB64,
NotBeforeScaled: int64(notBeforeScaled),
NotAfterScaled: int64(notAfterScaled),
SignatureAlgorithm: int64(signatureAlgorithm),
SignatureB64: signatureB64,
}
return &result, nil
}
// DehydrateCert dehydrates a certificate. If the input certificate is not
// compatible with the Dehydrated TLS Certificates specification, the resulting
// dehydrated certificate will not rehydrate into a valid x509 certificate.
func DehydrateCert(cert *x509.Certificate) (*DehydratedCertificate, error) {
pubkeyBytes, err := x509.MarshalPKIXPublicKey(cert.PublicKey)
if err != nil {
return nil, fmt.Errorf("failed to marshal parsed public key: %s", err)
}
pubkeyB64 := base64.StdEncoding.EncodeToString(pubkeyBytes)
notBeforeInt := cert.NotBefore.Unix()
notAfterInt := cert.NotAfter.Unix()
timestampPrecision := int64(5 * 60) // 5 minute precision
notBeforeScaled := notBeforeInt / timestampPrecision
notAfterScaled := notAfterInt / timestampPrecision
signatureAlgorithm := int64(cert.SignatureAlgorithm)
signatureBytes := cert.Signature
signatureB64 := base64.StdEncoding.EncodeToString(signatureBytes)
result := DehydratedCertificate{
PubkeyB64: pubkeyB64,
NotBeforeScaled: notBeforeScaled,
NotAfterScaled: notAfterScaled,
SignatureAlgorithm: signatureAlgorithm,
SignatureB64: signatureB64,
}
return &result, nil
}
// RehydrateCert converts a dehydrated certificate into a standard x509
// certificate, but does not fill in the domain name or any fields that depend
// on it. The resulting certificate is intended to be used as input to
// FillRehydratedCertTemplate.
func RehydrateCert(dehydrated *DehydratedCertificate) (*x509.Certificate, error) {
pubkeyBin, err := base64.StdEncoding.DecodeString(dehydrated.PubkeyB64)
if err != nil {
return nil, fmt.Errorf("Dehydrated cert pubkey must be valid base64: %s", err)
}
pubkey, err := x509.ParsePKIXPublicKey(pubkeyBin)
if err != nil {
return nil, fmt.Errorf("Dehydrated cert pubkey is invalid: %s", err)
}
timestampPrecision := int64(5 * 60) // 5 minute precision
notBeforeInt := dehydrated.NotBeforeScaled * timestampPrecision
notAfterInt := dehydrated.NotAfterScaled * timestampPrecision
notBefore := time.Unix(int64(notBeforeInt), 0)
notAfter := time.Unix(int64(notAfterInt), 0)
signatureAlgorithm := x509.SignatureAlgorithm(dehydrated.SignatureAlgorithm)
signature, err := base64.StdEncoding.DecodeString(dehydrated.SignatureB64)
if err != nil {
return nil, fmt.Errorf("Dehydrated cert signature must be valid base64: %s", err)
}
template := x509.Certificate{
SerialNumber: big.NewInt(1),
NotBefore: notBefore,
NotAfter: notAfter,
// x509.KeyUsageKeyEncipherment is used for RSA key exchange, but not DHE/ECDHE key exchange. Since everyone should be using ECDHE (due to forward secrecy), we disallow x509.KeyUsageKeyEncipherment in our template.
//KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
SignatureAlgorithm: signatureAlgorithm,
PublicKey: pubkey,
Signature: signature,
}
return &template, nil
}
// FillRehydratedCertTemplate fills in the domain name (and all dependent
// fields) to an x509 certificate that was returned by RehydrateCert, and
// returns a []byte suitable for inserting into a TLS trust store.
func FillRehydratedCertTemplate(template x509.Certificate, name string) ([]byte, error) {
template.Subject = pkix.Name{
CommonName: name,
SerialNumber: "Namecoin TLS Certificate",
}
// DNS name
template.DNSNames = append(template.DNSNames, name)
// Serial number
dehydrated, err := DehydrateCert(&template)
if err != nil {
return nil, fmt.Errorf("Error dehydrating filled cert template: %s", err)
}
serialNumberBytes, err := dehydrated.SerialNumber(name)
if err != nil {
return nil, fmt.Errorf("Error calculating serial number: %s", err)
}
template.SerialNumber.SetBytes(serialNumberBytes)
pub := template.PublicKey
priv := &splicesign.SpliceSigner{
PublicKey: pub,
Signature: template.Signature,
}
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, pub, priv)
if err != nil {
return nil, fmt.Errorf("Error splicing signature: %s", err)
}
return derBytes, nil
}