mirror of https://github.com/namecoin/ncdns
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.
237 lines
6.8 KiB
Go
237 lines
6.8 KiB
Go
package certdehydrate
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"crypto/x509/pkix"
|
|
"encoding/base64"
|
|
"encoding/binary"
|
|
"encoding/json"
|
|
"fmt"
|
|
"math/big"
|
|
"time"
|
|
)
|
|
|
|
import "github.com/namecoin/ncdns/x509"
|
|
|
|
// TODO: add a version field
|
|
type DehydratedCertificate struct {
|
|
PubkeyB64 string
|
|
NotBeforeScaled int64
|
|
NotAfterScaled int64
|
|
SignatureAlgorithm int64
|
|
SignatureB64 string
|
|
}
|
|
|
|
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()
|
|
serialHash.Write(nameHash[:])
|
|
serialHash.Write(pubkeyHash[:])
|
|
serialHash.Write(notBeforeHash[:])
|
|
serialHash.Write(notAfterHash[:])
|
|
|
|
// 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}
|
|
binOutput, _ := json.Marshal(output)
|
|
return string(binOutput)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// Accepts as input the bare minimum data needed to produce a valid cert.
|
|
// The input is untrusted.
|
|
// The output is safe.
|
|
// The timestamps are in 5-minute increments.
|
|
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
|
|
}
|
|
|
|
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)
|
|
|
|
derBytes, err := x509.CreateCertificateWithSplicedSignature(&template, &template)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Error splicing signature: %s", err)
|
|
}
|
|
|
|
return derBytes, nil
|
|
|
|
}
|