mirror of
https://github.com/slackhq/nebula.git
synced 2026-11-15 15:06:52 +01:00
443 lines
15 KiB
Go
443 lines
15 KiB
Go
package cert
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"slices"
|
|
"strings"
|
|
|
|
"golang.org/x/crypto/ed25519"
|
|
)
|
|
|
|
var ErrTruncatedPEMBlock = errors.New("truncated PEM block")
|
|
|
|
// SplitPEM is a split function for bufio.Scanner that returns each PEM block.
|
|
func SplitPEM(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
|
// Look for the start of a PEM block
|
|
start := bytes.Index(data, []byte("-----BEGIN "))
|
|
if start == -1 {
|
|
if atEOF && len(bytes.TrimSpace(data)) > 0 {
|
|
// Non-whitespace content with no PEM block
|
|
return 0, nil, ErrTruncatedPEMBlock
|
|
}
|
|
if atEOF {
|
|
return len(data), nil, nil
|
|
}
|
|
// Request more data
|
|
return 0, nil, nil
|
|
}
|
|
|
|
// Look for the end marker
|
|
endMarkerStart := bytes.Index(data[start:], []byte("-----END "))
|
|
if endMarkerStart == -1 {
|
|
if atEOF {
|
|
// Incomplete PEM block at EOF
|
|
return 0, nil, ErrTruncatedPEMBlock
|
|
}
|
|
// Need more data to find the end
|
|
return 0, nil, nil
|
|
}
|
|
|
|
// Find the actual end of the END line (after the newline)
|
|
endMarkerStart += start
|
|
endLineEnd := bytes.IndexByte(data[endMarkerStart:], '\n')
|
|
var end int
|
|
if endLineEnd == -1 {
|
|
if atEOF {
|
|
// END marker without newline at EOF - take it anyway
|
|
end = len(data)
|
|
} else {
|
|
// Need more data
|
|
return 0, nil, nil
|
|
}
|
|
} else {
|
|
end = endMarkerStart + endLineEnd + 1
|
|
}
|
|
|
|
// Extract the PEM block
|
|
pemBlock := data[start:end]
|
|
|
|
// Return the valid PEM block
|
|
return end, pemBlock, nil
|
|
}
|
|
|
|
const ( //cert banners
|
|
CertificateBanner = "NEBULA CERTIFICATE"
|
|
CertificateV2Banner = "NEBULA CERTIFICATE V2"
|
|
CertificateV3Banner = "NEBULA CERTIFICATE V3"
|
|
)
|
|
|
|
const ( //key-agreement-key banners
|
|
X25519PrivateKeyBanner = "NEBULA X25519 PRIVATE KEY"
|
|
X25519PublicKeyBanner = "NEBULA X25519 PUBLIC KEY"
|
|
P256PrivateKeyBanner = "NEBULA P256 PRIVATE KEY"
|
|
P256PublicKeyBanner = "NEBULA P256 PUBLIC KEY"
|
|
)
|
|
|
|
// Version 3 keys are composite: the classical key followed by the post-quantum key of the certificate's PQSuite, a
|
|
// KEM seed or public key for hosts and a signature algorithm's seed or public key for CAs. Their banners name both
|
|
// halves, "NEBULA X25519 MLKEM1024 PRIVATE KEY" or "NEBULA ED25519 MLDSA87 PUBLIC KEY" for example, because the
|
|
// post-quantum algorithm can not be told from the key's size: ML-KEM and ML-DSA seeds are the same size at every
|
|
// strength. The banners name algorithms, not suites, so suites sharing an algorithm share its banners too.
|
|
|
|
// Classical key sizes, the composite keys of version 3 add the post-quantum key to these.
|
|
const (
|
|
x25519KeySize = 32
|
|
p256PublicKeySize = 65 // uncompressed
|
|
p256PrivateKeySize = 32
|
|
ed25519PublicKeySize = 32
|
|
ed25519PrivateKeySize = 64
|
|
ecdsaP256PublicKeySize = 65 // uncompressed
|
|
)
|
|
|
|
// ClassicalPrivateKey returns the classical half of a host private key: all of a classical key, or the classical half
|
|
// of a composite one, whatever its suite.
|
|
func ClassicalPrivateKey(key []byte) []byte {
|
|
// X25519 and P256 private keys are the same size.
|
|
if len(key) > x25519KeySize {
|
|
return key[:x25519KeySize]
|
|
}
|
|
return key
|
|
}
|
|
|
|
// pqKeyKind is what a composite key banner holds.
|
|
type pqKeyKind int
|
|
|
|
const (
|
|
pqPrivateKey pqKeyKind = iota
|
|
pqPublicKey
|
|
pqSigningPrivateKey
|
|
pqSigningPublicKey
|
|
pqEncryptedSigningPrivateKey
|
|
)
|
|
|
|
// pqKeyBanner is what a composite key banner says about the key under it.
|
|
type pqKeyBanner struct {
|
|
kind pqKeyKind
|
|
curve Curve
|
|
size int // of the whole composite key; for an encrypted key, once decrypted
|
|
}
|
|
|
|
var pqKeyKinds = []pqKeyKind{pqPrivateKey, pqPublicKey, pqSigningPrivateKey, pqSigningPublicKey, pqEncryptedSigningPrivateKey}
|
|
|
|
// pqKeyBanners maps every composite key banner of every suite to what it holds, see registerPQSuite.
|
|
var pqKeyBanners = map[string]pqKeyBanner{}
|
|
|
|
// pqBannerName returns the banner for a composite key.
|
|
func pqBannerName(kind pqKeyKind, curve Curve, suite *pqSuite) string {
|
|
classical := map[Curve]string{Curve_CURVE25519: "X25519", Curve_P256: "P256"}[curve]
|
|
alg := suite.kem.name
|
|
if kind != pqPrivateKey && kind != pqPublicKey {
|
|
classical = map[Curve]string{Curve_CURVE25519: "ED25519", Curve_P256: "ECDSA P256"}[curve]
|
|
alg = suite.dsa.name
|
|
}
|
|
return "NEBULA " + classical + " " + alg + " " + map[pqKeyKind]string{
|
|
pqPrivateKey: "PRIVATE KEY",
|
|
pqPublicKey: "PUBLIC KEY",
|
|
pqSigningPrivateKey: "PRIVATE KEY",
|
|
pqSigningPublicKey: "PUBLIC KEY",
|
|
pqEncryptedSigningPrivateKey: "ENCRYPTED PRIVATE KEY",
|
|
}[kind]
|
|
}
|
|
|
|
// pqKeySize returns the size of a composite key.
|
|
func pqKeySize(kind pqKeyKind, curve Curve, suite *pqSuite) int {
|
|
p256 := curve == Curve_P256
|
|
switch kind {
|
|
case pqPrivateKey:
|
|
return x25519KeySize + suite.kem.privateKeySize // a P256 private key is the same size
|
|
case pqPublicKey:
|
|
return map[bool]int{false: x25519KeySize, true: p256PublicKeySize}[p256] + suite.kem.publicKeySize
|
|
case pqSigningPublicKey:
|
|
return map[bool]int{false: ed25519PublicKeySize, true: ecdsaP256PublicKeySize}[p256] + suite.dsa.publicKeySize
|
|
default:
|
|
return map[bool]int{false: ed25519PrivateKeySize, true: p256PrivateKeySize}[p256] + suite.dsa.privateKeySize
|
|
}
|
|
}
|
|
|
|
// marshalPQKeyToPEM PEM encodes a composite key of suite, nil for an unknown curve or suite or a key of the wrong size.
|
|
func marshalPQKeyToPEM(kind pqKeyKind, curve Curve, suite PQSuite, b []byte) []byte {
|
|
p := pqSuites[suite]
|
|
if p == nil || (curve != Curve_CURVE25519 && curve != Curve_P256) || len(b) != pqKeySize(kind, curve, p) {
|
|
return nil
|
|
}
|
|
return pem.EncodeToMemory(&pem.Block{Type: pqBannerName(kind, curve, p), Bytes: b})
|
|
}
|
|
|
|
// unmarshalPQKeyBlock checks a PEM block that may hold a composite key of one of kinds. ok is false when the banner
|
|
// is not a composite key banner of those kinds at all.
|
|
func unmarshalPQKeyBlock(k *pem.Block, kinds ...pqKeyKind) (curve Curve, ok bool, err error) {
|
|
b, found := pqKeyBanners[k.Type]
|
|
if !found || !slices.Contains(kinds, b.kind) {
|
|
return 0, false, nil
|
|
}
|
|
if b.kind != pqEncryptedSigningPrivateKey && len(k.Bytes) != b.size {
|
|
return b.curve, true, fmt.Errorf("key was not %d bytes, is invalid %s", b.size, strings.ToLower(strings.TrimPrefix(k.Type, "NEBULA ")))
|
|
}
|
|
return b.curve, true, nil
|
|
}
|
|
|
|
/* including "ECDSA" in the P256 banners is a clue that these keys should be used only for signing */
|
|
const ( //signing key banners
|
|
EncryptedECDSAP256PrivateKeyBanner = "NEBULA ECDSA P256 ENCRYPTED PRIVATE KEY"
|
|
ECDSAP256PrivateKeyBanner = "NEBULA ECDSA P256 PRIVATE KEY"
|
|
ECDSAP256PublicKeyBanner = "NEBULA ECDSA P256 PUBLIC KEY"
|
|
EncryptedEd25519PrivateKeyBanner = "NEBULA ED25519 ENCRYPTED PRIVATE KEY"
|
|
Ed25519PrivateKeyBanner = "NEBULA ED25519 PRIVATE KEY"
|
|
Ed25519PublicKeyBanner = "NEBULA ED25519 PUBLIC KEY"
|
|
)
|
|
|
|
// UnmarshalCertificateFromPEM will try to unmarshal the first pem block in a byte array, returning any non consumed
|
|
// data or an error on failure
|
|
func UnmarshalCertificateFromPEM(b []byte) (Certificate, []byte, error) {
|
|
p, r := pem.Decode(b)
|
|
if p == nil {
|
|
return nil, r, ErrInvalidPEMBlock
|
|
}
|
|
|
|
c, err := unmarshalCertificateBlock(p)
|
|
if err != nil {
|
|
return nil, r, err
|
|
}
|
|
|
|
return c, r, nil
|
|
|
|
}
|
|
|
|
// unmarshalCertificateBlock decodes a single PEM block into a certificate.
|
|
// It expects a Nebula certificate banner and returns ErrInvalidPEMCertificateBanner otherwise.
|
|
func unmarshalCertificateBlock(block *pem.Block) (Certificate, error) {
|
|
switch block.Type {
|
|
// Implementations must validate the resulting certificate contains valid information
|
|
case CertificateBanner:
|
|
return unmarshalCertificateV1(block.Bytes, nil)
|
|
case CertificateV2Banner:
|
|
return unmarshalCertificateV2(block.Bytes, nil, Curve_CURVE25519)
|
|
case CertificateV3Banner:
|
|
return unmarshalCertificateV3(block.Bytes, nil, Curve_CURVE25519)
|
|
default:
|
|
return nil, ErrInvalidPEMCertificateBanner
|
|
}
|
|
}
|
|
|
|
func marshalCertPublicKeyToPEM(c Certificate) []byte {
|
|
if suite := PQSuiteOf(c); suite != PQSuiteUnknown {
|
|
if c.IsCA() {
|
|
return MarshalPQSigningPublicKeyToPEM(c.Curve(), suite, c.PublicKey())
|
|
}
|
|
return MarshalPQPublicKeyToPEM(c.Curve(), suite, c.PublicKey())
|
|
}
|
|
if c.IsCA() {
|
|
return MarshalSigningPublicKeyToPEM(c.Curve(), c.PublicKey())
|
|
} else {
|
|
return MarshalPublicKeyToPEM(c.Curve(), c.PublicKey())
|
|
}
|
|
}
|
|
|
|
// MarshalPublicKeyToPEM returns a PEM representation of a public key used for ECDH.
|
|
// if your public key came from a certificate, prefer Certificate.PublicKeyPEM() if possible, to avoid mistakes!
|
|
func MarshalPublicKeyToPEM(curve Curve, b []byte) []byte {
|
|
switch curve {
|
|
case Curve_CURVE25519:
|
|
return pem.EncodeToMemory(&pem.Block{Type: X25519PublicKeyBanner, Bytes: b})
|
|
case Curve_P256:
|
|
return pem.EncodeToMemory(&pem.Block{Type: P256PublicKeyBanner, Bytes: b})
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// MarshalPQPublicKeyToPEM returns a PEM representation of a composite host public key of suite, the classical ECDH
|
|
// key followed by the suite's KEM public key, or nil if b is not one.
|
|
func MarshalPQPublicKeyToPEM(curve Curve, suite PQSuite, b []byte) []byte {
|
|
return marshalPQKeyToPEM(pqPublicKey, curve, suite, b)
|
|
}
|
|
|
|
// MarshalSigningPublicKeyToPEM returns a PEM representation of a public key used for signing.
|
|
// if your public key came from a certificate, prefer Certificate.PublicKeyPEM() if possible, to avoid mistakes!
|
|
func MarshalSigningPublicKeyToPEM(curve Curve, b []byte) []byte {
|
|
switch curve {
|
|
case Curve_CURVE25519:
|
|
return pem.EncodeToMemory(&pem.Block{Type: Ed25519PublicKeyBanner, Bytes: b})
|
|
case Curve_P256:
|
|
return pem.EncodeToMemory(&pem.Block{Type: ECDSAP256PublicKeyBanner, Bytes: b})
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// MarshalPQSigningPublicKeyToPEM returns a PEM representation of a composite CA public key of suite, the classical
|
|
// signing key followed by the public key of the suite's signature algorithm, or nil if b is not one.
|
|
func MarshalPQSigningPublicKeyToPEM(curve Curve, suite PQSuite, b []byte) []byte {
|
|
return marshalPQKeyToPEM(pqSigningPublicKey, curve, suite, b)
|
|
}
|
|
|
|
// UnmarshalPublicKeyFromPEM will try to unmarshal the first pem block in a byte array, returning any non
|
|
// consumed data or an error on failure. Only key-agreement (ECDH) public key banners are accepted.
|
|
// Use UnmarshalSigningPublicKeyFromPEM for Ed25519/ECDSA banners.
|
|
func UnmarshalPublicKeyFromPEM(b []byte) ([]byte, []byte, Curve, error) {
|
|
k, r := pem.Decode(b)
|
|
if k == nil {
|
|
return nil, r, 0, fmt.Errorf("input did not contain a valid PEM encoded block")
|
|
}
|
|
if curve, ok, err := unmarshalPQKeyBlock(k, pqPublicKey); ok {
|
|
if err != nil {
|
|
return nil, r, 0, err
|
|
}
|
|
return k.Bytes, r, curve, nil
|
|
}
|
|
var expectedLen int
|
|
var curve Curve
|
|
switch k.Type {
|
|
case X25519PublicKeyBanner:
|
|
expectedLen = 32
|
|
curve = Curve_CURVE25519
|
|
case P256PublicKeyBanner:
|
|
// Uncompressed
|
|
expectedLen = 65
|
|
curve = Curve_P256
|
|
default:
|
|
return nil, r, 0, fmt.Errorf("bytes did not contain a proper public key banner")
|
|
}
|
|
if len(k.Bytes) != expectedLen {
|
|
return nil, r, 0, fmt.Errorf("key was not %d bytes, is invalid %s public key", expectedLen, curve)
|
|
}
|
|
return k.Bytes, r, curve, nil
|
|
}
|
|
|
|
// UnmarshalSigningPublicKeyFromPEM will try to unmarshal the first pem block in a byte array, returning any non
|
|
// consumed data or an error on failure. Only Ed25519/ECDSA public key banners are accepted.
|
|
// Use UnmarshalPublicKeyFromPEM for X25519/P256 (ECDH) banners.
|
|
func UnmarshalSigningPublicKeyFromPEM(b []byte) ([]byte, []byte, Curve, error) {
|
|
k, r := pem.Decode(b)
|
|
if k == nil {
|
|
return nil, r, 0, fmt.Errorf("input did not contain a valid PEM encoded block")
|
|
}
|
|
if curve, ok, err := unmarshalPQKeyBlock(k, pqSigningPublicKey); ok {
|
|
if err != nil {
|
|
return nil, r, 0, err
|
|
}
|
|
return k.Bytes, r, curve, nil
|
|
}
|
|
var expectedLen int
|
|
var curve Curve
|
|
switch k.Type {
|
|
case Ed25519PublicKeyBanner:
|
|
expectedLen = 32
|
|
curve = Curve_CURVE25519
|
|
case ECDSAP256PublicKeyBanner:
|
|
// Uncompressed
|
|
expectedLen = 65
|
|
curve = Curve_P256
|
|
default:
|
|
return nil, r, 0, fmt.Errorf("bytes did not contain a proper Ed25519/ECDSA public key banner")
|
|
}
|
|
if len(k.Bytes) != expectedLen {
|
|
return nil, r, 0, fmt.Errorf("key was not %d bytes, is invalid %s public key", expectedLen, curve)
|
|
}
|
|
return k.Bytes, r, curve, nil
|
|
}
|
|
|
|
func MarshalPrivateKeyToPEM(curve Curve, b []byte) []byte {
|
|
switch curve {
|
|
case Curve_CURVE25519:
|
|
return pem.EncodeToMemory(&pem.Block{Type: X25519PrivateKeyBanner, Bytes: b})
|
|
case Curve_P256:
|
|
return pem.EncodeToMemory(&pem.Block{Type: P256PrivateKeyBanner, Bytes: b})
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// MarshalPQPrivateKeyToPEM returns a PEM representation of a composite host private key of suite, the classical ECDH
|
|
// key followed by the seed of the suite's KEM, or nil if b is not one.
|
|
func MarshalPQPrivateKeyToPEM(curve Curve, suite PQSuite, b []byte) []byte {
|
|
return marshalPQKeyToPEM(pqPrivateKey, curve, suite, b)
|
|
}
|
|
|
|
// MarshalPQSigningPrivateKeyToPEM returns a PEM representation of a composite CA private key of suite, the classical
|
|
// signing key followed by the seed of the suite's signature algorithm, or nil if b is not one.
|
|
func MarshalPQSigningPrivateKeyToPEM(curve Curve, suite PQSuite, b []byte) []byte {
|
|
return marshalPQKeyToPEM(pqSigningPrivateKey, curve, suite, b)
|
|
}
|
|
|
|
func MarshalSigningPrivateKeyToPEM(curve Curve, b []byte) []byte {
|
|
switch curve {
|
|
case Curve_CURVE25519:
|
|
return pem.EncodeToMemory(&pem.Block{Type: Ed25519PrivateKeyBanner, Bytes: b})
|
|
case Curve_P256:
|
|
return pem.EncodeToMemory(&pem.Block{Type: ECDSAP256PrivateKeyBanner, Bytes: b})
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// UnmarshalPrivateKeyFromPEM will try to unmarshal the first pem block in a byte array, returning any non
|
|
// consumed data or an error on failure
|
|
func UnmarshalPrivateKeyFromPEM(b []byte) ([]byte, []byte, Curve, error) {
|
|
k, r := pem.Decode(b)
|
|
if k == nil {
|
|
return nil, r, 0, fmt.Errorf("input did not contain a valid PEM encoded block")
|
|
}
|
|
if curve, ok, err := unmarshalPQKeyBlock(k, pqPrivateKey); ok {
|
|
if err != nil {
|
|
return nil, r, 0, err
|
|
}
|
|
return k.Bytes, r, curve, nil
|
|
}
|
|
var expectedLen int
|
|
var curve Curve
|
|
switch k.Type {
|
|
case X25519PrivateKeyBanner:
|
|
expectedLen = 32
|
|
curve = Curve_CURVE25519
|
|
case P256PrivateKeyBanner:
|
|
expectedLen = 32
|
|
curve = Curve_P256
|
|
default:
|
|
return nil, r, 0, fmt.Errorf("bytes did not contain a proper private key banner")
|
|
}
|
|
if len(k.Bytes) != expectedLen {
|
|
return nil, r, 0, fmt.Errorf("key was not %d bytes, is invalid %s private key", expectedLen, curve)
|
|
}
|
|
return k.Bytes, r, curve, nil
|
|
}
|
|
|
|
func UnmarshalSigningPrivateKeyFromPEM(b []byte) ([]byte, []byte, Curve, error) {
|
|
k, r := pem.Decode(b)
|
|
if k == nil {
|
|
return nil, r, 0, fmt.Errorf("input did not contain a valid PEM encoded block")
|
|
}
|
|
if curve, ok, err := unmarshalPQKeyBlock(k, pqSigningPrivateKey, pqEncryptedSigningPrivateKey); ok {
|
|
switch {
|
|
case err != nil:
|
|
return nil, r, 0, err
|
|
case pqKeyBanners[k.Type].kind == pqEncryptedSigningPrivateKey:
|
|
return nil, nil, curve, ErrPrivateKeyEncrypted
|
|
}
|
|
return k.Bytes, r, curve, nil
|
|
}
|
|
var curve Curve
|
|
switch k.Type {
|
|
case EncryptedEd25519PrivateKeyBanner:
|
|
return nil, nil, Curve_CURVE25519, ErrPrivateKeyEncrypted
|
|
case EncryptedECDSAP256PrivateKeyBanner:
|
|
return nil, nil, Curve_P256, ErrPrivateKeyEncrypted
|
|
case Ed25519PrivateKeyBanner:
|
|
curve = Curve_CURVE25519
|
|
if len(k.Bytes) != ed25519.PrivateKeySize {
|
|
return nil, r, 0, fmt.Errorf("key was not %d bytes, is invalid Ed25519 private key", ed25519.PrivateKeySize)
|
|
}
|
|
case ECDSAP256PrivateKeyBanner:
|
|
curve = Curve_P256
|
|
if len(k.Bytes) != 32 {
|
|
return nil, r, 0, fmt.Errorf("key was not 32 bytes, is invalid ECDSA P256 private key")
|
|
}
|
|
default:
|
|
return nil, r, 0, fmt.Errorf("bytes did not contain a proper Ed25519/ECDSA private key banner")
|
|
}
|
|
return k.Bytes, r, curve, nil
|
|
}
|